Hướng dẫn phòng chống SQL Injection hiệu quả cho websiteLink to heading
SQL Injection là một trong những hình thức tấn công phổ biến nhất, cho phép hacker khai thác lỗ hổng trong cơ sở dữ liệu để đánh cắp, chỉnh sửa hoặc xóa thông tin quan trọng. Nếu bạn đang vận hành một trang web, đặc biệt là website có tính năng đăng nhập hoặc quản lý dữ liệu người dùng và đang muốn phòng chống SQL Injection một cách hiệu quả và lâu dài để đảm bảo an toàn cho website của mình thì đây là bài viết dành cho bạn.
Tổng quan về SQL InjectionLink to heading
Tấn công SQL Injection (SQLi) là một hình thức tấn công mạng nguy hiểm, trong đó kẻ tấn công chèn các đoạn mã độc vào các trường nhập liệu của website nhằm khai thác lỗ hổng bảo mật trong ứng dụng web. Lỗ hổng này tạo điều kiện cho hacker chèn các câu lệnh SQL độc hại vào truy vấn gốc, khiến truy vấn bị thay đổi mục đích ban đầu. Nhờ đó, chúng có thể truy cập, đánh cắp hoặc thao túng dữ liệu trong cơ sở dữ liệu.
Khi hệ thống web xử lý các yêu cầu từ người dùng mà không có bước kiểm tra và lọc dữ liệu đầu vào một cách nghiêm ngặt, mã độc sẽ được thực thi trực tiếp trên máy chủ cơ sở dữ liệu, gây ra các cuộc tấn công SQL Injection nghiêm trọng.
Bằng cách tấn công SQL Injection, kẻ xấu có thể dễ dàng đánh cắp toàn bộ dữ liệu nhạy cảm như thông tin khách hàng, thẻ tín dụng, mật khẩu và dữ liệu kinh doanh mật. Nghiêm trọng hơn, chúng có thể kiểm soát hoàn toàn cơ sở dữ liệu và thậm chí là cả website, chèn mã độc hoặc phá hoại hệ thống. Điều này dẫn đến mất uy tín thương hiệu, thiệt hại tài chính khổng lồ và các vấn đề pháp lý nghiêm trọng.
Vậy làm thế nào để phòng chống SQL Injection một cách hiệu quả? Hãy đọc tiếp bài viết để biết nhé!
Cấu trúc điển hình của một lỗ hổng SQL InjectionLink to heading
Dưới đây là một ví dụ phổ biến về lỗi SQL Injection trong ngôn ngữ Java:
String query = "SELECT account_balance FROM user_data WHERE user_name = "
+ request.getParameter("customerName");
try {
Statement statement = connection.createStatement( ... );
ResultSet results = statement.executeQuery( query );
}
...
Có thể thấy tham số “customerName” không được kiểm tra đầu vào mà được ghép trực tiếp vào câu truy vấn SQL. Điều này cho phép kẻ tấn công chèn đoạn mã SQL vào tham số đó và ứng dụng sẽ thực thi mã độc ngay trên cơ sở dữ liệu.
4 Cách phòng chống SQL Injection hiệu quảLink to heading
Sử dụng Prepared Statements (với Parameterized Queries)Link to heading
Khi lập trình viên viết truy vấn cơ sở dữ liệu, họ cần sử dụng prepared statements — tức là truy vấn có gắn biến tham số. Đây là cách viết vừa đơn giản, dễ hiểu hơn so với truy vấn động, vừa buộc lập trình viên phải xác định sẵn toàn bộ câu lệnh SQL, sau đó mới truyền từng tham số vào.
Khi bạn viết truy vấn theo kiểu này, cơ sở dữ liệu sẽ luôn phân biệt rõ đâu là mã lệnh SQL, đâu là dữ liệu đầu vào — bất kể người dùng nhập gì. Điều này giúp ngăn chặn việc kẻ tấn công lợi dụng các đầu vào để "chèn" thêm câu lệnh SQL độc hại vào hệ thống. Prepared statements khiến hacker không thể thay đổi mục đích ban đầu của truy vấn, kể cả khi họ cố tình thêm các đoạn mã SQL vào dữ liệu nhập vào.
Ví dụ: Prepared Statement trong Java
Ví dụ sau đây minh họa cách sử dụng PreparedStatement trong Java — một cách triển khai phổ biến của truy vấn tham số hóa. Nếu kẻ tấn công nhập vào userID là tom' or '1'='1, thì truy vấn có tham số hóa sẽ tìm kiếm một tên người dùng trùng khớp chính xác với chuỗi tom' or '1'='1. Như vậy, cơ sở dữ liệu sẽ được bảo vệ khỏi việc chèn mã SQL độc hại. Nhờ đó, truy vấn không bị sửa đổi và cơ sở dữ liệu vẫn an toàn khỏi việc chèn mã độc.
Đoạn mã dưới đây sử dụng PreparedStatement — cách triển khai truy vấn tham số hóa của Java — để thực hiện cùng một truy vấn cơ sở dữ liệu.
// This should REALLY be validated too
String custname = request.getParameter("customerName");
// Perform input validation to detect attacks
String query = "SELECT account_balance FROM user_data WHERE user_name = ? ";
PreparedStatement pstmt = connection.prepareStatement( query );
pstmt.setString( 1, custname);
ResultSet results = pstmt.executeQuery( );
Ví dụ: Prepared Statement trong C# .NET
Trong .NET, cách tạo và thực thi truy vấn không thay đổi, chỉ cần truyền tham số vào truy vấn thông qua hàm Parameters.Add() như ví dụ dưới đây.
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
try {
OleDbCommand command = new OleDbCommand(query, connection);
command.Parameters.Add(new OleDbParameter("customerName", CustomerName Name.Text));
OleDbDataReader reader = command.ExecuteReader();
// …
} catch (OleDbException se) {
// error handling
}
Mặc dù các ví dụ được trình bày bằng Java và .NET, hầu hết các ngôn ngữ lập trình khác (kể cả Cold Fusion và Classic ASP) đều hỗ trợ truy vấn có tham số. Ngay cả các lớp trừu tượng SQL như Hibernate Query Language (HQL) — vốn cũng có thể bị tấn công HQL Injection — cũng hỗ trợ cơ chế truy vấn có tham số.
Ví dụ: Prepared Statement trong HQL
// This is an unsafe HQL statement
Query unsafeHQLQuery = session.createQuery("from Inventory where productID='"+userSuppliedParameter+"'");
// Here is a safe version of the same query using named parameters
Query safeHQLQuery = session.createQuery("from Inventory where productID=:productid");
safeHQLQuery.setParameter("productid", userSuppliedParameter);
Sử dụng Stored ProceduresLink to heading
Mặc dù Stored Procedures không hoàn toàn miễn nhiễm với SQL Injection, lập trình viên vẫn có thể áp dụng một số cấu trúc lập trình chuẩn trong Stored Procedure để phòng chống SQL Injection. Nếu được triển khai đúng cách thì phương pháp này mang lại hiệu quả tương đương với việc sử dụng truy vấn có tham số hóa (parameterized queries).
Cách sử dụng Stored Procedure
Trong trường hợp cần dùng Stored Procedure, cách an toàn nhất là lập trình viên nên xây dựng các câu lệnh SQL có sử dụng tham số và được tham số hóa tự động, trừ khi họ chủ động thực hiện một số thao tác sai lệch so với chuẩn thông thường.
Điểm khác biệt giữa Prepared Statement và Stored Procedure nằm ở chỗ: với Stored Procedure, phần mã SQL được định nghĩa và lưu trực tiếp trong cơ sở dữ liệu, sau đó được gọi từ ứng dụng. Vì cả hai phương pháp này đều ngăn chặn SQL Injection hiệu quả như nhau, nên doanh nghiệp có thể lựa chọn tùy theo mô hình phù hợp.
Mặt khác, Stored Procedure đôi khi có thể làm tăng rủi ro khi hệ thống bị tấn công. Ví dụ, trong MS SQL Server, có ba vai trò mặc định chính: db_datareader, db_datawriter và db_owner. Trước khi Stored Procedure được áp dụng rộng rãi, quản trị viên thường cấp quyền db_datareader hoặc db_datawriter cho người dùng dịch vụ web, tùy thuộc vào nhu cầu truy cập dữ liệu.
Tuy nhiên, Stored Procedure lại cần quyền execute, mà quyền này không được thiết lập sẵn mặc định. Trong một số hệ thống mà việc quản lý người dùng bị giới hạn chỉ trong ba vai trò trên, các ứng dụng web buộc phải chạy dưới quyền db_owner để có thể thực thi Stored Procedure. Điều này có nghĩa là nếu máy chủ bị xâm nhập, kẻ tấn công sẽ nắm toàn quyền truy cập cơ sở dữ liệu – thay vì chỉ có thể đọc dữ liệu như trước đây.
Ví dụ: Stored Procedure trong Java
Đoạn mã sau minh họa cách sử dụng Stored Procedure trong Java thông qua giao diện CallableStatement. Stored Procedure có tên sp_getAccountBalance phải được tạo sẵn trong cơ sở dữ liệu và thực hiện chức năng tương tự như truy vấn mà đoạn code mẫu đang xử lý.
// This should REALLY be validated
String custname = request.getParameter("customerName");
try {
CallableStatement cs = connection.prepareCall("{call sp_getAccountBalance(?)}");
cs.setString(1, custname);
ResultSet results = cs.executeQuery();
// … result set handling
} catch (SQLException se) {
// … logging and error handling
}
Ví dụ: Stored Procedure trong VB .NET
Đoạn mã dưới đây sử dụng SqlCommand, một cách triển khai Stored Procedure trong .NET, để thực thi cùng một truy vấn cơ sở dữ liệu. Stored procedure sp_getAccountBalance phải được định nghĩa sẵn trong cơ sở dữ liệu và có chức năng giống như truy vấn đã nêu ở trên.
Try
Dim command As SqlCommand = new SqlCommand("sp_getAccountBalance", connection)
command.CommandType = CommandType.StoredProcedure
command.Parameters.Add(new SqlParameter("@CustomerName", CustomerName.Text))
Dim reader As SqlDataReader = command.ExecuteReader()
'...
Catch se As SqlException
'error handling
End Try
Xác thực đầu vào bằng danh sách cho phépLink to heading
Khi bạn phải xử lý những phần trong câu truy vấn SQL không thể sử dụng biến ràng buộc – như tên bảng, tên cột hoặc chỉ báo sắp xếp (ASC hoặc DESC) – thì xác thực đầu vào hoặc thiết kế lại truy vấn là giải pháp phù hợp nhất. Trong trường hợp cần sử dụng tên bảng hoặc tên cột, tốt nhất các giá trị này nên được xác định sẵn trong mã nguồn, không nên lấy từ thông tin do người dùng cung cấp.
Xác thực tên bảng an toàn
CẢNH BÁO: Việc sử dụng giá trị từ người dùng để chỉ định tên bảng hoặc tên cột là dấu hiệu của một thiết kế yếu và nên được viết lại toàn bộ nếu có thời gian. Nếu không thể làm điều đó, lập trình viên cần ánh xạ các giá trị từ tham số người dùng sang danh sách tên bảng hoặc cột hợp lệ đã được xác định sẵn, nhằm tránh việc đầu vào chưa được xác thực lọt vào truy vấn SQL.
Ví dụ:
String tableName;
switch(PARAM):
case "Value1": tableName = "fooTable";
break;
case "Value2": tableName = "barTable";
break;
...
default : throw new InputValidationException("unexpected value provided"
+ " for table name");
Vì tableName đã được xác định là một trong những giá trị hợp lệ và mong đợi cho tên bảng trong truy vấn, nên có thể chấp nhận nối trực tiếp vào truy vấn SQL. Tuy nhiên, cần lưu ý rằng các hàm xác thực tên bảng mang tính tổng quát có thể gây mất dữ liệu nếu tên bảng bị sử dụng ở những vị trí không phù hợp trong câu truy vấn.
Sử dụng Dynamic SQL (Không khuyến khích)
Khi nói rằng một Stored Procedure được “triển khai an toàn”, điều đó có nghĩa là bên trong nó không có bất kỳ thao tác tạo câu lệnh SQL động nào mang tính rủi ro. Thông thường, các lập trình viên không tạo SQL động trong Stored Procedure. Tuy nhiên, điều đó vẫn có thể xảy ra – dù nên tránh nếu có thể.
Nếu việc dùng SQL động là không thể tránh khỏi, Stored Procedure bắt buộc phải sử dụng các biện pháp kiểm tra đầu vào hoặc cơ chế mã hóa hợp lý. Mục đích là đảm bảo rằng mọi dữ liệu đầu vào do người dùng cung cấp đều không thể bị lợi dụng để chèn mã SQL độc hại vào câu lệnh truy vấn được tạo động.
Trong quá trình kiểm tra, các chuyên viên đánh giá bảo mật cần đặc biệt chú ý đến các lệnh như sp_execute, execute hoặc exec trong stored procedure của SQL Server. Những chức năng tương tự trong hệ quản trị cơ sở dữ liệu khác cũng cần được kiểm tra theo tiêu chuẩn tương ứng.
Tạo truy vấn động (Không khuyến khích)
Với các nhu cầu đơn giản như sắp xếp theo thứ tự tăng hoặc giảm, tốt nhất là chuyển dữ liệu đầu vào từ người dùng thành giá trị boolean, sau đó dùng giá trị đó để chọn phần nội dung an toàn cần ghép vào câu truy vấn. Đây là một kỹ thuật phổ biến khi làm việc với các câu lệnh SQL động.
Ví dụ:
public String someMethod(boolean sortOrder) {
String SQLquery = "some SQL ... order by Salary " + (sortOrder ? "ASC" : "DESC");`
...
Bất cứ khi nào dữ liệu người dùng có thể được chuyển đổi sang kiểu không phải chuỗi, như ngày tháng, số, boolean hay kiểu liệt kê… trước khi được thêm vào truy vấn hoặc dùng để chọn giá trị sẽ thêm vào truy vấn, thì điều đó giúp đảm bảo an toàn.
Dù đã sử dụng biến liên kết (bind variables) như đã đề cập trước đó, việc xác thực đầu vào vẫn nên được áp dụng như một lớp phòng thủ bổ sung trong mọi trường hợp.
Loại bỏ toàn bộ dữ liệu người dùng cung cấp (không khuyến khích)Link to heading
Lập trình viên sẽ loại bỏ tất cả dữ liệu đầu vào từ người dùng trước khi đưa vào truy vấn. Tuy nhiên, cách làm này phụ thuộc rất nhiều vào loại cơ sở dữ liệu sử dụng. So với các phương pháp phòng thủ khác, cách này yếu hơn nhiều và KHÔNG thể đảm bảo ngăn chặn hoàn toàn các cuộc tấn công SQL Injection trong mọi tình huống.
Nếu bạn xây dựng một ứng dụng mới hoàn toàn hoặc ứng dụng yêu cầu mức độ rủi ro cực thấp, thì nên phát triển lại bằng cách sử dụng truy vấn có tham số (parameterized queries), stored procedures, hoặc áp dụng ORM (Object Relational Mapper) để tự động tạo truy vấn thay vì viết thủ công.
>>> Có thể bạn quan tâm: Check SQL Injection: Phát hiện & ngăn chặn các mối đe dọa
Các biện pháp phòng ngừa bổ sungLink to heading
Ngoài việc áp dụng một trong bốn cách trên, bạn cũng nên triển khai thêm các biện pháp phòng chống SQL Injection bổ sung dưới đây để tăng cường an ninh cho website của mình:
Nguyên tắc phân quyền tối thiểuLink to heading
Để giảm thiểu thiệt hại nếu bị tấn công SQL injection, bạn cần giới hạn tối đa quyền truy cập được cấp cho các tài khoản cơ sở dữ liệu trong hệ thống. Thay vì tìm cách loại bỏ quyền không cần thiết sau khi đã cấp, hãy xác định ngay từ đầu những quyền nào thực sự cần cho từng tài khoản mà ứng dụng sử dụng.
Với những tài khoản chỉ cần đọc dữ liệu, hãy chỉ cấp quyền đọc đối với các bảng cần thiết. Tuyệt đối không cấp quyền DBA hoặc Admin cho tài khoản ứng dụng. Dù rằng cách này giúp mọi thứ hoạt động “trơn tru”, nhưng lại cực kỳ nguy hiểm về mặt bảo mật.
Giới hạn quyền của ứng dụng và hệ điều hành
SQL injection không phải là mối đe dọa duy nhất đối với dữ liệu cơ sở dữ liệu. Kẻ tấn công có thể thay đổi giá trị tham số từ một giá trị hợp lệ thành một giá trị trái phép mà chính ứng dụng vẫn có thể truy cập được. Do đó, việc giới hạn quyền truy cập của ứng dụng sẽ giúp giảm rủi ro truy cập trái phép ngay cả khi không có hành vi SQL injection xảy ra.
Cùng lúc đó, bạn cũng nên hạn chế quyền của tài khoản hệ điều hành mà DBMS đang chạy. Không nên để DBMS hoạt động dưới tài khoản root hoặc system! Phần lớn DBMS khi cài đặt mặc định thường chạy với tài khoản hệ thống có quyền rất cao – chẳng hạn, MySQL trên Windows sẽ mặc định chạy bằng tài khoản system! Bạn cần thay đổi tài khoản đó thành một tài khoản khác với quyền hạn tối thiểu phù hợp.
Phân quyền khi phát triển ứng dụng
Nếu tài khoản chỉ cần truy cập một phần dữ liệu trong bảng, hãy tạo một view chỉ hiển thị phần đó và chỉ cấp quyền cho tài khoản truy cập vào view, thay vì bảng gốc. Hạn chế cấp quyền create hoặc delete cho bất kỳ tài khoản cơ sở dữ liệu nào (tốt nhất là không cấp để phòng chống SQL Injection).
Nếu bạn áp dụng chính sách sử dụng hoàn toàn Stored Procedure thay vì để tài khoản ứng dụng thực thi trực tiếp các truy vấn SQL, thì hãy giới hạn quyền của tài khoản đó chỉ trong phạm vi thực thi các thủ tục cụ thể cần thiết. Không cấp bất kỳ quyền truy cập trực tiếp nào đến các bảng dữ liệu cho các tài khoản này.
Tách biệt tài khoản admin cho các ứng dụng web khác nhau
Nhà phát triển web nên tránh việc sử dụng cùng một tài khoản chủ sở hữu hoặc admin để kết nối đến cơ sở dữ liệu trong nhiều ứng dụng web. Thay vào đó, nên tạo tài khoản cơ sở dữ liệu riêng biệt cho từng ứng dụng.
Mỗi ứng dụng web riêng biệt cần truy cập cơ sở dữ liệu nên có tài khoản chuyên biệt để kết nối. Cách làm này giúp nhà phát triển dễ dàng kiểm soát phân quyền chi tiết, đồng thời giới hạn quyền ở mức thấp nhất có thể. Mỗi tài khoản sẽ chỉ có quyền đọc ghi đúng với vai trò được thiết kế.
Tăng cường nguyên tắc đặc quyền tối thiểu bằng SQL View
Bạn có thể sử dụng SQL View để kiểm soát quyền truy cập dữ liệu một cách chi tiết hơn, bằng cách giới hạn quyền đọc chỉ với một số trường nhất định trong bảng hoặc trong các truy vấn join giữa nhiều bảng. Phương pháp phòng chống SQL Injection này còn mang lại thêm một số lợi ích khác.
Ví dụ, nếu hệ thống vì lý do pháp lý buộc phải lưu trữ mật khẩu người dùng (thay vì sử dụng mật khẩu đã được mã hóa dạng hash với salt), nhà thiết kế có thể dùng View để khắc phục điểm yếu này. Họ có thể thu hồi toàn bộ quyền truy cập vào bảng chứa mật khẩu (trừ quyền của chủ sở hữu hoặc quản trị viên) và tạo ra một View chỉ hiển thị giá trị hash của trường mật khẩu thay vì hiển thị trực tiếp giá trị gốc.
Ngay cả khi có một cuộc tấn công SQL Injection thành công, kẻ tấn công cũng chỉ có thể lấy được chuỗi hash của mật khẩu (thậm chí có thể là hash có khóa), vì tất cả người dùng của các ứng dụng web đều không có quyền truy cập trực tiếp vào bảng gốc.
Xác thực đầu vào theo danh sách cho phépLink to heading
Ngoài việc là tuyến phòng thủ chính trong trường hợp không thể sử dụng các biến ràng buộc, xác thực đầu vào còn có thể được dùng như một lớp bảo vệ bổ sung, giúp phát hiện dữ liệu không hợp lệ trước khi truyền vào truy vấn SQL. Tuy nhiên, cần thận trọng: dữ liệu đã được xác thực không đồng nghĩa với việc an toàn tuyệt đối nếu đưa trực tiếp vào truy vấn SQL thông qua cách nối chuỗi.
Kiểm tra mã độc định kỳLink to heading
Ngoài việc áp dụng các biện pháp phòng chống SQL Injection nêu trên, bạn cũng nên triển khai quy trình kiểm tra mã độc định kỳ cho toàn bộ hệ thống website và cơ sở dữ liệu. Điều này đặc biệt quan trọng vì trong nhiều trường hợp, các cuộc tấn công SQL Injection không chỉ dừng lại ở việc đánh cắp dữ liệu, mà còn có thể cài cắm mã độc hoặc backdoor để tiếp tục chiếm quyền kiểm soát hệ thống sau này.
Để giúp các nhà quản trị, chủ website kịp thời phát hiện và xử lý mã độc nhanh chóng, System443 đã nghiên cứu và phát triển một công cụ quét mã độc hoàn toàn miễn phí. Chỉ cần nhập tên miền website, bạn đã có thể kiểm tra nhanh tình trạng website của mình.
>>> Kiểm tra mã độc TẠI ĐÂY!
Kết luậnLink to heading
Việc phòng chống SQL Injection không chỉ là nhiệm vụ của riêng lập trình viên, mà là trách nhiệm chung của toàn bộ hệ thống quản lý và vận hành website. Các cuộc tấn công có thể xảy ra bất kỳ lúc nào nếu bạn lơ là trong việc kiểm soát dữ liệu đầu vào, thiếu các lớp bảo mật cơ bản hoặc không kiểm tra mã độc định kỳ. Hãy chủ động triển khai các biện pháp phòng ngừa sớm, kết hợp công cụ giám sát và quét mã độc để bảo vệ website một cách toàn diện và bền vững.
Bạn muốn biết thêm về các lỗ hổng bảo mật phổ biến? Khám phá ngay loạt bài viết chuyên sâu của System443!

