Tổng quan
Trong Hệ thống quản lý cơ sở dữ liệu quan hệ (RDBMS), có một ngôn ngữ cụ thể — được gọi là SQL (Ngôn ngữ truy vấn có cấu trúc) — được sử dụng để giao tiếp với cơ sở dữ liệu. Các câu lệnh truy vấn được viết bằng SQL được sử dụng để thao tác nội dung và cấu trúc của cơ sở dữ liệu. Một câu lệnh SQL cụ thể tạo và sửa đổi cấu trúc của cơ sở dữ liệu được gọi là câu lệnh DDL (Ngôn ngữ định nghĩa dữ liệu) và các câu lệnh thao tác nội dung cơ sở dữ liệu được gọi là câu lệnh DML (Ngôn ngữ thao tác dữ liệu). Công cụ được liên kết với gói RDBMS phân tích cú pháp và diễn giải câu lệnh SQL và trả về kết quả tương ứng. Đây là quy trình giao tiếp điển hình với RDBMS — kích hoạt một câu lệnh SQL và nhận lại kết quả, đó là tất cả. Hệ thống không đánh giá ý định của bất kỳ câu lệnh nào tuân theo cú pháp và cấu trúc ngữ nghĩa của ngôn ngữ. Điều này cũng có nghĩa là không có quy trình xác thực hoặc xác thực nào để kiểm tra xem ai đã kích hoạt câu lệnh và người đó có đặc quyền khi nhận đầu ra. Kẻ tấn công có thể chỉ cần bắn một câu lệnh SQL với mục đích xấu và lấy lại thông tin mà nó không được phép lấy. Ví dụ:kẻ tấn công có thể thực thi một câu lệnh SQL có tải trọng độc hại với truy vấn trông vô hại để kiểm soát máy chủ cơ sở dữ liệu của ứng dụng Web.
Cách thức hoạt động
Kẻ tấn công có thể tận dụng lỗ hổng này và sử dụng nó cho lợi thế của riêng một người. Ví dụ:người ta có thể bỏ qua cơ chế xác thực và ủy quyền của ứng dụng và truy xuất cái gọi là nội dung an toàn từ toàn bộ cơ sở dữ liệu. Một SQL injection có thể được sử dụng để tạo, cập nhật và xóa các bản ghi khỏi cơ sở dữ liệu. Do đó, người ta có thể tạo ra một truy vấn giới hạn trong trí tưởng tượng của riêng người ta với SQL.
Thông thường, một ứng dụng thường xuyên kích hoạt các truy vấn SQL đến cơ sở dữ liệu cho nhiều mục đích, có thể là để tìm nạp các bản ghi nhất định, tạo báo cáo, xác thực người dùng, giao dịch CRUD, v.v. Kẻ tấn công chỉ cần tìm một truy vấn đầu vào SQL trong một số biểu mẫu nhập ứng dụng. Sau đó, truy vấn được chuẩn bị bởi biểu mẫu có thể được sử dụng để tinh chỉnh nội dung độc hại để khi ứng dụng kích hoạt truy vấn, nó cũng mang theo trọng tải được đưa vào.
Một trong những tình huống lý tưởng là khi ứng dụng yêu cầu người dùng nhập thông tin đầu vào như tên người dùng hoặc id người dùng. Ứng dụng đã mở ra một điểm dễ bị tấn công ở đó. Câu lệnh SQL có thể được chạy một cách vô tình. Kẻ tấn công lợi dụng bằng cách đưa vào một trọng tải được sử dụng như một phần của truy vấn SQL và được xử lý bởi cơ sở dữ liệu. Ví dụ:mã giả phía máy chủ cho thao tác ĐĂNG cho biểu mẫu đăng nhập có thể là:
uname = getRequestString("username"); pass = getRequestString("passwd"); stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "'"; database.execute(stmtSQL);
Mã trước dễ bị tấn công chèn SQL vì đầu vào được cung cấp cho câu lệnh SQL thông qua biến ‘uname’ và ‘pass’ có thể được thao tác theo cách có thể làm thay đổi ngữ nghĩa của câu lệnh.
Ví dụ:chúng ta có thể sửa đổi truy vấn để chạy trên máy chủ cơ sở dữ liệu, như trong MySQL.
stmtSQL = "SELECT * FROM users WHERE user_name = '" + uname + "' AND passwd = '" + pass + "' OR 1=1";
Điều này dẫn đến việc sửa đổi câu lệnh SQL ban đầu ở mức độ cho phép một câu lệnh bỏ qua xác thực. Đây là một lỗ hổng nghiêm trọng và phải được ngăn chặn từ trong mã.
Phòng thủ chống lại cuộc tấn công SQL Injection
Một trong những cách để giảm nguy cơ bị tấn công SQL injection là đảm bảo rằng các chuỗi văn bản chưa được lọc không được phép nối vào câu lệnh SQL trước khi thực thi. Ví dụ:chúng tôi có thể sử dụng PreparedStatement để thực hiện các tác vụ cơ sở dữ liệu cần thiết. Khía cạnh thú vị của PreparedStatement là nó gửi một câu lệnh SQL được biên dịch trước đến cơ sở dữ liệu, chứ không phải là một chuỗi. Điều này có nghĩa là truy vấn và dữ liệu được gửi riêng đến cơ sở dữ liệu. Điều này ngăn chặn nguyên nhân gốc rễ của cuộc tấn công SQL injection, bởi vì trong SQL injection, ý tưởng là trộn mã và dữ liệu, trong đó dữ liệu thực sự là một phần của mã trong vỏ bọc dữ liệu. Trong Câu lệnh chuẩn bị , có nhiều setXYZ () các phương thức, chẳng hạn như setString () . Các phương thức này được sử dụng để lọc các ký tự đặc biệt, chẳng hạn như dấu ngoặc kép chứa trong các câu lệnh SQL.
Ví dụ:chúng ta có thể thực thi một câu lệnh SQL theo cách sau.
String sql = "SELECT * FROM employees WHERE emp_no = "+eno;
Thay vì đặt, hãy nói, eno =10125 dưới dạng số nhân viên trong đầu vào, chúng tôi có thể sửa đổi truy vấn với đầu vào như:
eno = 10125 OR 1=1
Điều này thay đổi hoàn toàn kết quả được trả về bởi truy vấn.
Một ví dụ
Trong mã ví dụ sau, chúng tôi đã chỉ ra cách Chuẩn bị sẵn sàng có thể được sử dụng để thực hiện các tác vụ cơ sở dữ liệu.
package org.mano.example; import java.sql.*; import java.time.LocalDate; public class App { static final String JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; static final String DB_URL = "jdbc:mysql://localhost:3306/employees"; static final String USER = "root"; static final String PASS = "secret"; public static void main( String[] args ) { String selectQuery = "SELECT * FROM employees WHERE emp_no = ?"; String insertQuery = "INSERT INTO employees VALUES (?,?,?,?,?,?)"; String deleteQuery = "DELETE FROM employees WHERE emp_no = ?"; Connection connection = null; try { Class.forName(JDBC_DRIVER); connection = DriverManager.getConnection (DB_URL, USER, PASS); }catch(Exception ex) { ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(insertQuery);){ pstmt.setInt(1,99); pstmt.setDate(2, Date.valueOf (LocalDate.of(1975,12,11))); pstmt.setString(3,"ABC"); pstmt.setString(4,"XYZ"); pstmt.setString(5,"M"); pstmt.setDate(6,Date.valueOf(LocalDate.of(2011,1,1))); pstmt.executeUpdate(); System.out.println("Record inserted successfully."); }catch(SQLException ex){ ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(selectQuery);){ pstmt.setInt(1,99); ResultSet rs = pstmt.executeQuery(); while(rs.next()){ System.out.println(rs.getString(3)+ " "+rs.getString(4)); } }catch(Exception ex){ ex.printStackTrace(); } try(PreparedStatement pstmt = connection.prepareStatement(deleteQuery);){ pstmt.setInt(1,99); pstmt.executeUpdate(); System.out.println("Record deleted successfully."); }catch(SQLException ex){ ex.printStackTrace(); } try{ connection.close(); }catch(Exception ex){ ex.printStackTrace(); } } }
Cái nhìn sơ lược về Chuẩn bị sẵn sàng
Những công việc này cũng có thể được hoàn thành với một Tuyên bố của JDBC nhưng vấn đề là đôi khi nó có thể khá không an toàn, đặc biệt là khi một câu lệnh SQL động được thực thi để truy vấn cơ sở dữ liệu nơi các giá trị đầu vào của người dùng được nối với các truy vấn SQL. Đây có thể là một tình huống nguy hiểm, như chúng ta đã thấy. Trong hầu hết các trường hợp thông thường, Tuyên bố khá vô hại, nhưng Chuẩn bị sẵn sàng Có vẻ là lựa chọn tốt hơn giữa hai phương pháp này. Nó ngăn các chuỗi độc hại được nối với nhau do cách tiếp cận khác nhau trong việc gửi câu lệnh tới cơ sở dữ liệu. Chuẩn bị sẵn sàng sử dụng thay thế biến hơn là nối. Đặt dấu chấm hỏi (?) Trong truy vấn SQL biểu thị rằng một biến thay thế sẽ thay thế và cung cấp giá trị khi truy vấn được thực thi. Vị trí của biến thay thế sẽ diễn ra theo vị trí chỉ số tham số được chỉ định trong setXYZ () các phương pháp.
Kỹ thuật này ngăn không cho nó khỏi bị tấn công SQL injection.
Hơn nữa, PreparedStatement triển khai Tự động đóng. Điều này cho phép nó viết trong ngữ cảnh của một thử tài nguyên chặn và tự động đóng khi nó vượt ra khỏi phạm vi.
Kết luận
Một cuộc tấn công chèn SQL chỉ có thể được ngăn chặn bằng cách viết mã một cách có trách nhiệm. Trên thực tế, trong bất kỳ giải pháp phần mềm nào, bảo mật hầu hết đều bị vi phạm do thực hành mã hóa không tốt. Ở đây, chúng tôi đã mô tả những điều cần tránh và cách thực hiện Chuẩn bị sẵn sàng có thể giúp chúng tôi viết mã an toàn. Để có ý tưởng đầy đủ về SQL injection, hãy tham khảo các tài liệu thích hợp; Internet có đầy đủ chúng, và cho PreparedStatement , hãy xem Tài liệu Java API để có giải thích chi tiết hơn.