Reading Time: 9 minutes

Trong bài viết này, chúng ta sẽ đi từ định nghĩa cơ bản, tìm hiểu tác động nguy hiểm mà lỗ hổng SQL injection có thể gây ra, khám phá cách thức hoạt động của các loại SQL Injection khác nhau, và đặc biệt là xem xét một ví dụ cụ thể trong ứng dụng Java. Cuối cùng, chúng ta sẽ trang bị cho mình những phương pháp phòng chống hiệu quả nhất.

SQL Injection là gì?

SQL Injection là một trong số 10 lỗ hổng ứng dụng web hàng đầu (OWASP Top 10). Nói một cách đơn giản, SQL Injection là việc tiêm/chèn mã SQL vào một truy vấn thông qua dữ liệu do người dùng nhập vào. Lỗ hổng này có thể xảy ra trong bất kỳ ứng dụng nào sử dụng cơ sở dữ liệu quan hệ (relational databases) như Oracle, MySQL, PostgreSQL và SQL Server.

Để thực hiện SQL Injection, một kẻ tấn công độc hại trước tiên sẽ cố gắng tìm một vị trí trong ứng dụng mà họ có thể nhúng mã SQL cùng với dữ liệu. Đó có thể là trang đăng nhập của bất kỳ ứng dụng web nào hoặc bất kỳ nơi nào khác chấp nhận đầu vào của người dùng. Khi dữ liệu được nhúng mã SQL được ứng dụng nhận, mã SQL đó sẽ được thực thi cùng với truy vấn gốc của ứng dụng.

Tác động của SQL Injection

Một cuộc tấn công SQL Injection có thể gây ra hậu quả rất nghiêm trọng:

  • Một kẻ tấn công độc hại có thể truy cập trái phép vào ứng dụng của bạn và đánh cắp dữ liệu nhạy cảm (thông tin người dùng, mật khẩu, dữ liệu tài chính…).
  • Họ có thể thay đổi, xóa dữ liệu trong cơ sở dữ liệu của bạn, thậm chí đánh sập ứng dụng.
  • Một hacker còn có thể chiếm quyền kiểm soát hệ thống mà máy chủ cơ sở dữ liệu đang chạy bằng cách thực thi các lệnh hệ thống đặc thù của cơ sở dữ liệu.

SQL Injection hoạt động như thế nào?

Giả sử chúng ta có một bảng cơ sở dữ liệu tên là tbluser lưu trữ dữ liệu người dùng của ứng dụng. Cột userId là khóa chính của bảng. Chúng ta có một chức năng trong ứng dụng cho phép bạn lấy thông tin thông qua userId. Giá trị của userId được nhận từ yêu cầu của người dùng.

Hãy cùng xem xét đoạn mã ví dụ dưới đây:

String userId = {get data from end user};
String sqlQuery = "select * from tbluser where userId = " + userId;

1. Đầu vào hợp lệ từ người dùng

Khi truy vấn trên được thực thi với dữ liệu hợp lệ, tức là giá trị userId132, nó sẽ trông như sau: Dữ liệu đầu vào: 132

Truy vấn được thực thi: select * from tbluser where userId = 132

Kết quả: Truy vấn sẽ trả về dữ liệu của người dùng có userId132.

Không có SQL Injection nào xảy ra trong trường hợp này.

2. Đầu vào độc hại từ hacker

Một hacker có thể thay đổi yêu cầu của người dùng bằng cách sử dụng các công cụ như Postman, cURL, v.v., để gửi mã SQL dưới dạng dữ liệu, từ đó vượt qua bất kỳ xác thực nào ở phía giao diện người dùng (UI). Dữ liệu đầu vào: 2 or 1=1

Truy vấn được thực thi: select * from tbluser where userId = 2 or 1=1

Kết quả: Bây giờ, truy vấn trên có hai điều kiện với biểu thức SQL OR:

  • userId=2: Phần này sẽ khớp với các hàng trong bảng có giá trị userId là ‘2’.
  • 1=1: Phần này sẽ luôn được đánh giá là true (đúng). Vậy nên, truy vấn sẽ trả về tất cả các hàng của bảng. Đây là một ví dụ cơ bản về cách một kẻ tấn công có thể truy cập trái phép vào dữ liệu.

Các loại SQL Injection

Chúng ta hãy xem xét bốn loại SQL Injection phổ biến:

1. Boolean Based SQL Injection

Ví dụ trên là một trường hợp của Boolean Based SQL Injection. Nó sử dụng một biểu thức boolean (1=1 hoặc 1=0) để đánh giá true hoặc false. Nó có thể được sử dụng để lấy thêm thông tin từ cơ sở dữ liệu. Ví dụ: Dữ liệu đầu vào: 2 or 1=1

Truy vấn SQL: select first_name, last_name from tbl_employee where empId=2 or 1=1

2. Union Based SQL Injection

Toán tử UNION trong SQL kết hợp dữ liệu từ hai truy vấn khác nhau có cùng số lượng cột. Trong trường hợp này, toán tử UNION được sử dụng để lấy dữ liệu từ các bảng khác. Dữ liệu đầu vào: 2 union select username, password from tbluser

Truy vấn: Select first_name, last_name from tbl_employee where empId=2 union select username, password from tbluser Bằng cách sử dụng Union Based SQL Injection, một kẻ tấn công có thể lấy được thông tin đăng nhập của người dùng.

3. Time-Based SQL Injection

Trong Time Based SQL Injection, các hàm đặc biệt được chèn vào truy vấn có thể tạm dừng thực thi trong một khoảng thời gian xác định. Cuộc tấn công này làm chậm máy chủ cơ sở dữ liệu. Nó có thể làm sập ứng dụng của bạn bằng cách ảnh hưởng đến hiệu suất máy chủ cơ sở dữ liệu. Ví dụ, trong MySQL: Dữ liệu đầu vào: 2 + SLEEP(5)

Truy vấn: select emp_id, first_name, last_name from tbl_employee where empId=2 + SLEEP(5) Trong ví dụ trên, quá trình thực thi truy vấn sẽ tạm dừng trong 5 giây. Nếu kẻ tấn công có thể thay đổi thời gian tạm dừng và quan sát phản hồi, họ có thể suy ra thông tin từ cơ sở dữ liệu.

4. Error Based SQL Injection

Trong biến thể này, kẻ tấn công cố gắng lấy thông tin như mã lỗi và thông báo từ cơ sở dữ liệu. Kẻ tấn công chèn các câu lệnh SQL không đúng cú pháp, do đó máy chủ cơ sở dữ liệu sẽ trả về mã lỗi và thông báo mà có thể được sử dụng để lấy thông tin về cơ sở dữ liệu và hệ thống.

Ví dụ SQL Injection trong Java

Chúng ta sẽ sử dụng một ứng dụng Java Web đơn giản để minh họa SQL Injection. Chúng ta có Login.html, là một trang đăng nhập cơ bản nhận tên người dùng và mật khẩu từ người dùng và gửi chúng đến LoginServlet. LoginServlet lấy tên người dùng và mật khẩu từ yêu cầu và xác thực chúng với các giá trị trong cơ sở dữ liệu. Nếu xác thực thành công, Servlet sẽ chuyển hướng người dùng đến trang chủ, nếu không, nó sẽ trả về lỗi.

Login.html Code:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Sql Injection Demo</title>
    </head>
    <body>
    <form name="frmLogin" method="POST" action="<https://localhost:8080/Web1/LoginServlet>">
        <table>
            <tr>
                <td>Username</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>Password</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">Login</button></td>
            </tr>
        </table>
    </form>
    </body>
</html>

LoginServlet.java Code:

package com.journaldev.examples;
import java.io.IOException;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
    static {
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (Exception e) {}
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        boolean success = false;
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        // Truy vấn không an toàn sử dụng phép nối chuỗi
        String query = "select * from tbluser where username='" + username + "' and password = '" + password + "'";
        Connection conn = null;
        Statement stmt = null;
        try {
            conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            if (rs.next()) {
                // Đăng nhập thành công nếu tìm thấy khớp
                success = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                stmt.close();
                conn.close();
            } catch (Exception e) {}
        }
        if (success) {
            response.sendRedirect("home.html");
        } else {
            response.sendRedirect("login.html?error=1");
        }
    }
}

Database Queries [MySQL]:

create database user;

create table tbluser(username varchar(32) primary key, password varchar(32));

insert into tbluser (username,password) values ('john','secret');
insert into tbluser (username,password) values ('mike','pass10');

1. Khi tên người dùng và mật khẩu hợp lệ được nhập từ trang đăng nhập

Input username: john

Input password: secret

Truy vấn: select * from tbluser where username='john' and password = 'secret'

Kết quả: Tên người dùng và mật khẩu tồn tại trong cơ sở dữ liệu nên xác thực thành công. Người dùng sẽ được chuyển hướng đến trang chủ.

2. Truy cập trái phép vào hệ thống bằng SQL Injection

Input username: dummy

Input password: ' or '1'='1

Truy vấn: select * from tbluser where username='dummy' and password = '' or '1'='1'

Kết quả: Tên người dùng và mật khẩu đã nhập không tồn tại trong cơ sở dữ liệu nhưng xác thực vẫn thành công. Tại sao? Điều này là do SQL Injection, vì chúng ta đã nhập ' or '1'='1 làm mật khẩu. Có 3 điều kiện trong truy vấn:

  • username='dummy': Điều này sẽ được đánh giá là false vì không có người dùng nào có tên dummy trong bảng.
  • password = '': Điều này sẽ được đánh giá là false vì không có mật khẩu trống trong bảng.
  • '1'='1': Điều này sẽ được đánh giá là true vì đây là phép so sánh chuỗi tĩnh luôn đúng.

Bây giờ kết hợp cả 3 điều kiện: false AND false OR true => Kết quả cuối cùng sẽ là true. Trong kịch bản trên, chúng ta đã sử dụng biểu thức boolean để thực hiện SQL Injection. Có một số cách khác để thực hiện SQL Injection như đã đề cập ở trên.

Ngăn chặn SQL Injection trong Java Code

Giải pháp đơn giản và hiệu quả nhất là sử dụng PreparedStatement thay vì Statement khi thực thi truy vấn. Thay vì nối trực tiếp usernamepasswordvào truy vấn, chúng ta truyền chúng vào câu lệnh bằng các phương thức setter của PreparedStatement. Khi sử dụng PreparedStatement, giá trị usernamepassword nhận được từ request sẽ được xử lý như dữ liệu thuần túy, không phải là một phần của mã SQL, do đó ngăn chặn được SQL Injection.

Hãy cùng xem xét đoạn mã servlet đã được sửa đổi:

String query = "select * from tbluser where username=? and password = ?";
Connection conn = null;
PreparedStatement stmt = null; // Thay đổi từ Statement sang PreparedStatement
try {
    conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/user", "root", "root");
    stmt = conn.prepareStatement(query); // Sử dụng prepareStatement
    stmt.setString(1, username); // Đặt giá trị cho tham số vị trí thứ nhất
    stmt.setString(2, password); // Đặt giá trị cho tham số vị trí thứ hai
    ResultSet rs = stmt.executeQuery(); // Thực thi truy vấn
    if (rs.next()) {
        // Đăng nhập thành công nếu tìm thấy khớp
        success = true;
    }
    rs.close(); // Đóng ResultSet
} catch (Exception e) {
    e.printStackTrace();
} finally {
    try {
        if (stmt != null) stmt.close();
        if (conn != null) conn.close();
    } catch (Exception e) {
    }
}

Hãy cùng hiểu điều gì đang xảy ra trong trường hợp này: Truy vấn**:** select * from tbluser where username = ? and password = ? Dấu hỏi (?) trong truy vấn trên được gọi là tham số vị trí (positional parameter). Có 2 tham số vị trí trong truy vấn trên. Chúng ta không nối usernamepassword trực tiếp vào truy vấn. Chúng ta sử dụng các phương thức có sẵn trong PreparedStatement để nhận đầu vào của người dùng . Chúng ta đã đặt tham số thứ nhất bằng cách sử dụng stmt.setString(1, username) và tham số thứ hai bằng cách sử dụng stmt.setString(2, password). API JDBC bên dưới sẽ chịu trách nhiệm làm sạch các giá trị để tránh SQL Injection.

stmt.setString(1, username)stmt.setString(2, password)

Những phương pháp tốt nhất để tránh SQL Injection

Bên cạnh việc sử dụng PreparedStatement, đây là một số thực hành tốt nhất khác để tăng cường bảo mật cho ứng dụng của bạn:

  • Xác thực dữ liệu (Validate data) trước khi sử dụng chúng trong truy vấn. Luôn giả định đầu vào của người dùng là độc hại.
  • Không sử dụng các từ thông dụng làm tên bảng hoặc tên cột của bạn. Ví dụ, nhiều ứng dụng sử dụng tbluser hoặc tblaccount để lưu trữ dữ liệu người dùng. Email, firstname, lastname cũng là các tên cột phổ biến. Kẻ tấn công thường thử những cái tên này trước tiên.
  • Không trực tiếp nối dữ liệu (nhận được dưới dạng đầu vào của người dùng) để tạo truy vấn SQL. Luôn sử dụng các phương pháp an toàn như PreparedStatement.
  • Sử dụng các framework như Hibernate và Spring Data JPA cho tầng dữ liệu của ứng dụng. Các framework này thường tích hợp sẵn cơ chế bảo vệ SQL Injection bằng cách sử dụng PreparedStatement hoặc các kỹ thuật tương tự.
  • Giới hạn quyền truy cập của ứng dụng vào cơ sở dữ liệu thông qua các quyền (permissions) và cấp phép (grants) ở mức thấp nhất cần thiết (Least Privilege Principle). Ứng dụng chỉ nên có quyền truy cập vào những bảng và hoạt động mà nó thực sự cần.
  • Không trả về mã lỗi và thông báo lỗi nhạy cảm cho người dùng cuối. Thông báo lỗi chi tiết có thể cung cấp thông tin quý giá cho kẻ tấn công.
  • Thực hiện quy trình đánh giá mã (code review) chặt chẽ để không có lập trình viên nào vô tình viết mã SQL không an toàn.
  • Sử dụng các công cụ tự động như SQLMap để tìm và khắc phục các lỗ hổng SQL Injection trong ứng dụng của bạn.

Kết luận

SQL Injection là một mối đe dọa thực sự và thường xuyên xuất hiện trong thế giới phát triển ứng dụng web. Chúng ta đã cùng nhau tìm hiểu định nghĩa, các tác động khôn lường, cũng như cách thức hoạt động của bốn loại SQL Injection chính: Boolean Based, Union Based, Time-Based và Error Based. Điểm mấu chốt để phòng chống SQL Injection là không bao giờ tin tưởng đầu vào từ người dùng. Giải pháp vàng được nhấn mạnh ở đây chính là việc sử dụng PreparedStatement thay vì Statement trong Java JDBC. PreparedStatement giúp tách biệt mã SQL khỏi dữ liệu, đảm bảo rằng mọi đầu vào đều được xử lý như giá trị dữ liệu thuần túy chứ không phải là một phần của câu lệnh SQL.

Trong môi trường phát triển phần mềm hiện đại, việc hiểu và áp dụng các biện pháp bảo mật từ giai đoạn thiết kế đến triển khai là tối quan trọng. Hãy luôn cảnh giác, không ngừng học hỏi và áp dụng những kiến thức này vào các dự án của mình để góp phần xây dựng một thế giới số an toàn hơn.

0 Bình luận

Đăng nhập để thảo luận

Chuyên mục Hướng dẫn

Tổng hợp các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.

Đăng ký nhận bản tin của chúng tôi

Hãy trở thành người nhận được các nội dung hữu ích của CyStack sớm nhất

Xem chính sách của chúng tôi Chính sách bảo mật.

Đăng ký nhận Newsletter

Nhận các nội dung hữu ích mới nhất