Trong bài này
Một học viên mở trang hồ sơ và sửa mã trên đường dẫn sang mã của bạn khác. Một người khác thử gõ thêm phép so sánh vào ô tìm kiếm. Cả hai đều có thể làm lộ dữ liệu nếu ứng dụng chỉ tin những gì trình duyệt gửi lên. Cách xử lý hai tình huống này khác nhau.
Ghép chuỗi có thể biến dữ liệu thành điều kiện mới
Chèn lệnh SQL (SQL injection) xảy ra khi dữ liệu nhập tác động tới cấu trúc câu lệnh. Nguy cơ xuất hiện nếu chương trình ghép nguyên chuỗi người dùng nhập vào SQL. Một phép lọc dự định chọn một người có thể biến thành phép lọc giữ mọi người.
Giả sử chương trình nối phần WHERE id = với nội dung ô nhập. Khi nội dung là 1 OR 1=1, câu lệnh tạo ra tương đương câu sau:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Câu lệnh
SELECT id, name
FROM students
WHERE id = 1 OR 1 = 1
ORDER BY id;Kết quả
| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Kết quả có đủ sáu học viên. Vế 1 = 1 đúng trên từng dòng, nên OR giữ cả những dòng có mã khác 1. Đây không phải lỗi của phép OR, mà do chương trình cho dữ liệu nhập trở thành phép toán.
Với ô tìm tên, giả sử chương trình ghép WHERE name = ' với ô nhập rồi thêm dấu ' ở cuối. Nếu ô nhập là x' OR '1'='1, câu tạo ra sẽ là:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Câu lệnh
SELECT id, name
FROM students
WHERE name = 'x' OR '1'='1'
ORDER BY id;Kết quả
| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Kết quả vẫn có cả sáu học viên. Dấu nháy sau chữ x đã đóng chuỗi sớm, nên phần OR trở thành phép toán của câu lệnh. Dù không có ai tên x, vế '1'='1' vẫn giữ mọi dòng.
Nếu chuỗi 1 OR 1=1 được giữ như một giá trị, nó không còn tạo điều kiện mới. Câu sau biểu diễn phép so sánh với nguyên chuỗi chữ để bạn thấy sự khác biệt:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Câu lệnh
SELECT id, name
FROM students
WHERE id = '1 OR 1=1'
ORDER BY id;Kết quả
| id | name |
|---|---|
| Không có dòng nào | |
Kết quả rỗng vì không mã số nào bằng chuỗi chữ đó. Máy không đọc lại phần bên trong chuỗi như một đoạn SQL. Tuy nhiên, tự thêm dấu nháy bằng cách nối chuỗi vẫn dễ sai khi nội dung có dấu nháy riêng.
| Cách xử lý ô nhập | Điều máy nhận được | Vấn đề |
|---|---|---|
| Ghép nguyên chuỗi | Cấu trúc SQL mới | Người nhập có thể đổi điều kiện |
| Tự thêm dấu nháy | SQL tự ghép bằng tay | Dễ xử lý thiếu trường hợp đặc biệt |
| Gắn tham số bằng thư viện | Câu lệnh và giá trị riêng | Dữ liệu không đổi cấu trúc câu lệnh |
Chặn vài từ như OR không giải quyết nguyên nhân. Tên người thật cũng có thể chứa dấu nháy hoặc ký tự bất ngờ. Thư viện cần nhận giá trị ở kênh riêng thay vì để ứng dụng tự đoán cách sửa từng ký tự.
Tham số giữ cấu trúc cố định khi giá trị thay đổi
Tham số (parameter) là vị trí dành cho một giá trị được truyền riêng khi thực thi. Trong phòng SQL, :viewer_id là tham số mã người xem mẫu. Giá trị mẫu là 1, không phải danh tính người đang đăng nhập website.
Ví dụ, câu đọc có dạng SELECT id, name FROM students WHERE id = :viewer_id;. Trong thư viện Python sqlite3, chương trình có thể gắn giá trị theo tên như sau:
sql = "SELECT id, name FROM students WHERE id = :viewer_id"
rows = db.execute(sql, {"viewer_id": 1}).fetchall()
Biến db ở đây là kết nối tới bộ dữ liệu học tập. Thư viện nhận nguyên câu SQL và một tập giá trị riêng. Thay số trong tập giá trị không làm xuất hiện thêm từ khóa hay phép toán trong câu lệnh.
Khối sau giữ nguyên tham số trong câu lệnh và gắn riêng giá trị mẫu 1 khi chạy:
Dữ liệu đầu vào
studentsgiữ 1/6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Câu lệnh
SELECT id, name
FROM students
WHERE id = :viewer_id;Giá trị gắn riêng: :viewer_id = 1
Kết quả
| id | name |
|---|---|
| 1 | An |
Kết quả có hồ sơ An vì mã của An bằng 1. Khi đổi giá trị mẫu thành 2, điều kiện chọn hồ sơ Bình. Phần SQL có thể được giữ nguyên trong ứng dụng, chỉ tập giá trị truyền cho thư viện thay đổi.
Thử gắn riêng một chuỗi có dấu nháy và phép OR làm giá trị tên cần tìm:
Dữ liệu đầu vào
students6 dòng| id | name |
|---|---|
| 1 | An |
| 2 | Bình |
| 3 | Chi |
| 4 | Dung |
| 5 | Huy |
| 6 | Lan |
Câu lệnh
SELECT id, name
FROM students
WHERE name = :keyword;Giá trị gắn riêng: :keyword = "An' OR '1'='1"
Kết quả
| id | name |
|---|---|
| Không có dòng nào | |
Kết quả rỗng vì không ai có tên là toàn bộ chuỗi An' OR '1'='1. Dấu nháy và chữ OR vẫn nằm trong giá trị được gắn. Chúng không đóng chuỗi hay thêm điều kiện như cách ghép trực tiếp ở mục trước.
Câu lệnhCấu trúc do chương trình viếtGiá trịDữ liệu truyền riêngThư việnGắn giá trị vào vị tríThực thiGiữ ý nghĩa câu lệnh
Khi dùng dấu ? của Python sqlite3, mỗi vị trí nhận một giá trị từ bộ giá trị đi kèm. Một giá trị đơn cần được truyền trong bộ một phần tử, chẳng hạn (1,). Đây là quy ước của thư viện, khác với việc dùng chuỗi đã ghép sẵn.
Giá trị gắn tham số vẫn cần đúng kiểu và phạm vi mà chức năng cho phép. Một mã hợp lệ về cú pháp có thể là mã của người khác. Truyền tham số giải quyết việc giữ cấu trúc câu lệnh, chưa trả lời câu hỏi ai được đọc hồ sơ nào.
Tên cột thuộc cấu trúc, không phải giá trị để gắn tùy ý
Ô chọn cách sắp xếp cũng gửi dữ liệu từ trình duyệt. Nhưng tên định danh (identifier), như tên bảng hoặc cột, thuộc cấu trúc SQL. Tham số giá trị không tự biến thành tên cột.
Nếu ứng dụng truyền chữ coins vào vị trí giá trị sau ORDER BY, đó là một giá trị không đổi trên mọi dòng. Hãy xem phép sắp xếp tương đương với chuỗi hằng:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
SELECT id, name, coins
FROM students
ORDER BY 'coins', id;Kết quả
| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Dòng của An đứng trước Bình vì cột phụ id quyết định thứ tự. Chuỗi 'coins' giống nhau cho mọi học viên, nên không giúp xếp theo số xu. Dấu nháy đơn tạo giá trị chữ, không chọn cột có cùng tên.
Muốn xếp theo số xu, cấu trúc câu lệnh phải thực sự nhắc tới cột đó:
Dữ liệu đầu vào
students6 dòng| id | name | coins |
|---|---|---|
| 1 | An | 120 |
| 2 | Bình | 90 |
| 3 | Chi | 180 |
| 4 | Dung | 60 |
| 5 | Huy | 180 |
| 6 | Lan | 150 |
Câu lệnh
SELECT id, name, coins
FROM students
ORDER BY coins, id;Kết quả
| id | name | coins |
|---|---|---|
| 4 | Dung | 60 |
| 2 | Bình | 90 |
| 1 | An | 120 |
| 6 | Lan | 150 |
| 3 | Chi | 180 |
| 5 | Huy | 180 |
Dung có 60 xu nên đứng đầu, khác hẳn cách xếp theo mã. Đây là lý do không thay ORDER BY coins bằng một tham số chứa chữ coins. Hai biểu thức có vai trò khác nhau dù nhìn cùng một từ.
- Với giá trị tìm kiếm, dùng tham số mà thư viện hỗ trợ.
- Với lựa chọn sắp xếp, ánh xạ lựa chọn sang một đoạn SQL do chương trình chuẩn bị.
- Với lựa chọn không nằm trong danh sách cho phép, từ chối hoặc dùng cách sắp xếp mặc định đã công bố.
Danh sách cho phép (allowlist) là tập những lựa chọn ứng dụng chấp nhận. Chẳng hạn, giao diện chỉ cho sắp theo tên hoặc số xu. Máy chủ chọn một trong hai đoạn SQL đã viết, thay vì chèn nguyên tên cột người dùng tự gửi.
Tham số không thay thế xác thực và phân quyền
Xác thực (authentication) xác định người đang gửi yêu cầu. Phân quyền (authorization) quyết định người ấy được làm gì với dữ liệu nào. Một yêu cầu dùng tham số đúng vẫn có thể đọc sai người nếu máy chủ tin mã do trình duyệt tự khai.
Xem trường hợp người xem mẫu có mã 1 nhưng câu đọc không lọc chủ sở hữu:
Dữ liệu đầu vào
scores7 dòng| id | student_id | score |
|---|---|---|
| 1 | 1 | 80 |
| 2 | 1 | 90 |
| 3 | 2 | 70 |
| 4 | 3 | 90 |
| 5 | 3 | 100 |
| 6 | 4 | 50 |
| 7 | 5 | 90 |
Câu lệnh
SELECT id, student_id, score
FROM scores
ORDER BY id;Kết quả
| id | student_id | score |
|---|---|---|
| 1 | 1 | 80 |
| 2 | 1 | 90 |
| 3 | 2 | 70 |
| 4 | 3 | 90 |
| 5 | 3 | 100 |
| 6 | 4 | 50 |
| 7 | 5 | 90 |
Kết quả chứa cả điểm của các học viên khác. Không có thao tác chèn lệnh nào ở đây, câu SQL vốn đã cho đọc rộng. Vì vậy chỉ kiểm rằng truy vấn dùng tham số an toàn chưa đủ để bảo vệ dữ liệu cá nhân.
Trong một ứng dụng thật, máy chủ lấy danh tính từ phiên đăng nhập đã xác minh. Nếu trang chỉ cho xem điểm của chính mình, máy chủ dùng mã đáng tin cậy đó để giới hạn dòng. Với giá trị mẫu 1, phần dữ liệu cần trả tương ứng là:
Dữ liệu đầu vào
scores7 dòng| id | student_id | score |
|---|---|---|
| 1 | 1 | 80 |
| 2 | 1 | 90 |
| 3 | 2 | 70 |
| 4 | 3 | 90 |
| 5 | 3 | 100 |
| 6 | 4 | 50 |
| 7 | 5 | 90 |
Câu lệnh
SELECT id, student_id, score
FROM scores
WHERE student_id = :viewer_id
ORDER BY id;Giá trị gắn riêng: :viewer_id = 1
Kết quả
| id | student_id | score |
|---|---|---|
| 1 | 1 | 80 |
| 2 | 1 | 90 |
Bảng chỉ còn hai lần chấm của An. Đây là minh họa phép lọc, chưa phải hệ xác thực hay chính sách bảo mật. Người tự viết SQL vẫn có thể bỏ điều kiện, nên phòng học không dùng phép lọc này để chứng minh bảo vệ dữ liệu thật.
Trong PostgreSQL, GRANT cấp quyền thao tác trên đối tượng cho vai trò (role). Bảo mật từng dòng, gọi là row-level security hoặc RLS, thêm chính sách quyết định dòng nào được đọc hay sửa. Hai lớp này không tự thay thế việc xác minh người dùng trong ứng dụng.
Các câu lệnh sau minh họa quyền đọc và chính sách cho vai trò learner trên PostgreSQL:
| Câu lệnh PostgreSQL | Tác dụng |
|---|---|
GRANT SELECT ON scores TO learner; | Cho vai trò học viên quyền đọc bảng điểm |
ALTER TABLE scores ENABLE ROW LEVEL SECURITY; | Bật kiểm chính sách từng dòng trên bảng điểm |
CREATE POLICY own_scores ON scores FOR SELECT USING (student_id = current_learner_id()); | Chỉ cho đọc điểm có mã học viên khớp danh tính đã xác minh |
current_learner_id() là hàm giả định của ứng dụng, lấy mã học viên từ phiên đã xác minh. PostgreSQL không có sẵn hàm này. Ứng dụng phải triển khai và kiểm cách cung cấp danh tính ấy trên hệ quản trị thật.
| Lớp kiểm | Câu hỏi nó giải quyết | Điều nó chưa chứng minh |
|---|---|---|
| Tham số | Giá trị có đổi cấu trúc SQL không? | Người gửi có được đọc hồ sơ đó không? |
GRANT | Vai trò có được đọc đối tượng không? | Mọi dòng trong đối tượng có phù hợp không? |
RLS | Dòng nào thỏa chính sách của vai trò? | Danh tính được truyền vào có đáng tin không? |
Khi bật RLS mà chưa có chính sách cho phép, người dùng thông thường có thể không thấy dòng nào. Chủ bảng thường được miễn chính sách, và một số vai trò đặc quyền có thể bỏ qua nó. Vì vậy thử bằng chủ bảng rồi thấy đủ dữ liệu chưa chứng minh chính sách cho học viên đúng.
Một lần kiểm thực tế nên có hai người dùng thường khác nhau. Thử người thứ nhất đọc dữ liệu của mình và đọc mã người thứ hai, rồi đổi vai. Đồng thời kiểm đường gọi khác có thể trả cùng dữ liệu, thay vì chỉ kiểm một trang giao diện.
Lỗi thường gặp
| Lỗi | Vì sao sai | Cách sửa |
|---|---|---|
| Ghép ô nhập vào SQL | Dữ liệu có thể trở thành phép toán | Gắn giá trị bằng tham số |
| Truyền tên cột bằng tham số giá trị | Chuỗi tên cột không trở thành định danh | Ánh xạ lựa chọn qua danh sách cho phép |
| Tin mã người xem từ trình duyệt | Người gửi có thể chọn mã khác | Dùng danh tính đã xác minh và kiểm quyền |
| Thử chính sách chỉ bằng chủ bảng | Vai trò đặc quyền có thể được miễn | Kiểm thêm các vai trò người dùng thường |
Tóm tắt
- Giữ câu SQL và dữ liệu nhập ở hai kênh riêng khi thực thi.
- Tham số thay giá trị, còn tên đối tượng thuộc cấu trúc do chương trình kiểm soát.
- Truy vấn an toàn trước chèn lệnh vẫn cần giới hạn dữ liệu theo quyền đọc.
GRANTvàRLSgiải quyết hai phạm vi khác nhau trong PostgreSQL.- Kết quả lọc ở SQLite không chứng minh đã kiểm chính sách bảo mật PostgreSQL.