SQL Injection là gì và cách phòng chống nó?
Phân tích thực tế về các cuộc tấn công SQL injection, ví dụ thực tế, và các biện pháp khắc phục cụ thể hoạt động trong mã production.
SQL injection tồn tại từ những năm cuối 1990 và nó vẫn là một trong những cách phổ biến nhất để các ứng dụng web bị xâm phạm. Lỗi này rất đơn giản: kẻ tấn công gửi input được hiểu là mã SQL thay vì dữ liệu thuần tuý, và cơ sở dữ liệu thực hiện một thứ nó không bao giờ được phép làm.
Cuộc tấn công hoạt động như thế nào
Tưởng tượng một biểu mẫu đăng nhập xây dựng một truy vấn như thế này trong PHP:
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
Nếu ứng dụng không làm sạch input, kẻ tấn công nhập admin' -- vào trường username. Truy vấn trở thành:
SELECT * FROM users WHERE username = 'admin' --' AND password = ''
-- bình luận phần còn lại của dòng, vì vậy kiểm tra mật khẩu không bao giờ xảy ra. Đó là một cuộc vượt qua xác thực cổ điển. Kẻ tấn công cũng sử dụng UNION SELECT để lấy dữ liệu từ các bảng khác, hoặc xếp chồng các truy vấn với dấu chấm phẩy để chạy thứ gì đó phá hủy như DROP TABLE users; nếu driver cho phép nhiều câu lệnh.
Có cũng là blind SQL injection, nơi ứng dụng không hiển thị kết quả truy vấn trực tiếp. Kẻ tấn công suy ra thông tin thông qua timing (SLEEP(5) trong MySQL) hoặc phản hồi boolean (trang có hoạt động khác nhau cho điều kiện đúng hay sai không). Các công cụ như sqlmap tự động hóa loại trích xuất này sau khi tìm thấy tham số dễ bị tấn công.
Tại sao concatenation chuỗi là vấn đề gốc rễ
Mọi biến thể của cuộc tấn công này đều quay về một điều: trộn mã và dữ liệu trong cùng một chuỗi. Cơ sở dữ liệu không thể phân biệt giữa một giá trị hợp pháp và cú pháp tiêm nhiễm vì chúng đến qua cùng một kênh. Escaping dấu ngoặc kép giúp được trong một số trường hợp nhưng nó không bền vững — mã hóa khác nhau, second-order injection (dữ liệu được lưu trữ một lần, sau đó được sử dụng lại không an toàn sau này), và những điểm lạ do driver tạo ra đều tạo ra những đường vượt qua. Escaping là một bản vá, không phải một bản sửa.
Parameterized queries là bản sửa thực sự
Bản sửa là tách mã SQL khỏi dữ liệu của người dùng ở cấp độ driver, sử dụng parameterized queries (còn gọi là prepared statements). Cơ sở dữ liệu nhận cấu trúc truy vấn trước, sau đó liên kết các giá trị sau, vì vậy input của người dùng không bao giờ có thể thay đổi ý nghĩa của truy vấn.
Trong Python với psycopg2:
cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))
Trong Node.js với mysql2:
connection.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);
Trong Java với JDBC:
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);
Chú ý mẫu: placeholders (%s, ?) giữ chỗ cho dữ liệu, và các giá trị thực tế được truyền riêng. Không có concatenation chuỗi, không cần escaping thủ công. Điều này hoạt động cho phần lớn các truy vấn bạn sẽ viết trong một ứng dụng bình thường.
Nếu tên bảng hoặc cột là động?
Parameterization xử lý các giá trị nhưng không xử lý các định danh — bạn không thể liên kết tên bảng làm tham số. Nếu ứng dụng của bạn cần chọn một bảng động (hiếm, và thường là một mùi thiết kế), whitelist các giá trị được phép so với một danh sách được mã hóa cứng thay vì tin tưởng input của người dùng trực tiếp:
allowed_tables = {'orders', 'invoices', 'customers'}
if table_name not in allowed_tables:
raise ValueError("Invalid table")
Nicely xây dựng tên định danh thông qua định dạng chuỗi từ input của người dùng, ngay cả với escaping.
Phòng chống phân tầng ngoài bản thân truy vấn
Parameterized queries là điều khiển chính, nhưng một vài điều khác cũng quan trọng:
- Least privilege trên tài khoản cơ sở dữ liệu. Người dùng DB của ứng dụng không được phép có
DROP,ALTER, hoặc truy cập vào các schema không liên quan. Nếu một injection xảy ra, các quyền hạn chế sẽ hạn chế thiệt hại. - ORMs giúp theo mặc định. Django ORM, SQLAlchemy, và Hibernate đều tham số hóa các truy vấn tự động khi bạn sử dụng các phương thức xây dựng truy vấn tiêu chuẩn của chúng. Rủi ro xuất hiện khi các nhà phát triển rơi vào raw SQL hoặc sử dụng các cuộc gọi
.extra()/text()với string interpolation — vì vậy hãy kiểm toán các vị trí đó cụ thể. - Input validation là một lớp thứ cấp, không phải sự thay thế. Kiểm tra xem trường email có trông giống email không là một thực hành tốt, nhưng nó không dừng injection tự nó — kẻ tấn công tìm những payload sáng tạo vẫn vượt qua xác thực lỏng lẻo.
- WAF có thể bắt các mẫu tấn công đã biết, nhưng nó là một lớp phát hiện, không phải một bản sửa cho mã cơ bản.
Kiểm tra mã của riêng bạn cho lỗi này
Chạy các truy vấn của bạn qua một công cụ phân tích tĩnh (Bandit cho Python, Semgrep với các bộ quy tắc SQL injection) như một phần của CI. Để kiểm tra thủ công, hãy thử tiêm một dấu ngoặc kép đơn (') vào mọi trường input và xem liệu các thông báo lỗi SQL có rò rỉ trong phản hồi không — đó thường là dấu hiệu đầu tiên rằng một truy vấn không được tham số hóa.
Nếu bạn muốn đi sâu hơn về điều này, Web Security track của Korra Studio bao gồm injection bên cạnh XSS và auth bypass, và các phân đoạn Databases hướng dẫn các mẫu thiết kế truy vấn tránh hoàn toàn lớp lỗi này.
Viết với hỗ trợ của AI, được xem xét và đăng bởi Michal Pilch (CISSP), Korra Studio.
Đây là một ghi chép từ cơ sở kiến thức Korra Studio — nền tảng kết hợp mỗi chủ đề với phiên hỗ trợ 1-kèm-1.
Bắt đầu miễn phíarrow_forward