Classic SQLi: Vì sao chuỗi biến thành câu lệnh trong Lexer
Mổ xẻ quá trình Lexer và Parser phân tích câu lệnh SQL: vì sao việc ghép chuỗi khiến dữ liệu người dùng biến thành cấu trúc cú pháp của câu truy vấn.
TL;DR: SQL Injection thường được giới thiệu một cách hời hợt qua payload kinh điển ' OR 1=1 --, nhưng ít ai giải thích điều gì thực sự diễn ra bên trong engine cơ sở dữ liệu. Khi nhận một câu truy vấn thô được tạo bằng cách nối chuỗi, bộ tách từ (Lexer) và bộ phân tích cú pháp (Parser) của cơ sở dữ liệu không thể phân biệt đâu là ý đồ của lập trình viên và đâu là dữ liệu của người dùng. Ký tự nháy đơn đóng chuỗi dữ liệu sớm, biến chuỗi ký tự theo sau thành các token từ khóa (keywords) và toán tử điều khiển cây cú pháp trừu tượng (AST). Cơ chế Prepared Statements giải quyết triệt để vấn đề này bằng cách biên dịch cây AST trước, cố định cấu trúc câu lệnh và đối xử với mọi tham số truyền vào thuần túy là dữ liệu vô hại.
1. Database Engine xử lý và biên dịch câu lệnh SQL theo chu trình nào?
Khi ứng dụng backend (viết bằng Go, Node.js, hay Python như đã làm quen ở 03-curl-va-python-automation-co-ban) gửi một chuỗi văn bản truy vấn sang hệ quản trị cơ sở dữ liệu (PostgreSQL, MySQL), máy chủ cơ sở dữ liệu không thực thi ngay lập tức. Nó phải đưa chuỗi qua một chuỗi xử lý tương tự như một trình biên dịch ngôn ngữ:
- Bộ tách từ (Lexical Analyzer / Lexer): Đọc chuỗi ký tự thô và nhóm chúng thành các Tokens có ý nghĩa (Từ khóa như
SELECT,FROM,WHERE; Tên bảng/cột; Toán tử=,AND,OR; Hằng số chuỗi hoặc số). - Bộ phân tích cú pháp (Syntax Parser): Kiểm tra xem các token có tuân thủ đúng ngữ pháp SQL hay không và xây dựng Cây Cú pháp Trừu tượng (Abstract Syntax Tree - AST).
- Bộ tối ưu hóa truy vấn (Query Optimizer): Lập kế hoạch thực thi (Execution Plan) hiệu quả nhất (dùng Index quét bảng hay tuần tự).
- Bộ máy thực thi (Execution Engine): Truy xuất và thao tác các trang dữ liệu trên đĩa/RAM theo AST đã tối ưu.

Hình 3.3: Dữ liệu người dùng chứa dấu nháy đơn làm thay đổi cấu trúc cây Abstract Syntax Tree (AST) trong khâu phân tích cú pháp.
2. Vì sao ký tự nháy đơn phá vỡ cây cú pháp trừu tượng (AST)?
Hãy quan sát một đoạn mã xác thực đăng nhập backend sai lầm điển hình bằng cách ghép chuỗi:
// Ghep chuoi truc tiep tao cau lenh SQL
const query = "SELECT * FROM users WHERE username = '" + req.body.username + "' AND password = '" + req.body.password + "'";
Trường hợp người dùng hợp lệ
Khi người dùng nhập username: alice và password: secret123:
SELECT * FROM users WHERE username = 'alice' AND password = 'secret123'
Lexer phân tách câu lệnh thành các token:
- Keywords:
SELECT,FROM,WHERE,AND - Identifiers:
users,username,password - Literals (Dữ liệu chuỗi thuần túy):
'alice','secret123'
Cây AST được hình thành với mệnh đề điều kiện: username bằng 'alice' VÀ password bằng 'secret123'.
Trường hợp kẻ tấn công can thiệp
Nếu kẻ tấn công nhập vào ô username chuỗi: ' OR 1=1 --:
Chuỗi hoàn chỉnh được gửi tới database là:
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = '...'
Lúc này, Lexer nhìn thấy dấu nháy đơn đầu tiên và hiểu rằng: "Hằng số chuỗi cho cột username đã kết thúc tại đây!".
Ký tự tiếp theo là chữ OR, Lexer nhận diện nó không phải là dữ liệu chuỗi mà là Keyword toán tử logic OR!
Tiếp đến 1=1 trở thành một biểu thức so sánh luôn đúng (TRUE).
Đặc biệt, ký tự comment -- ra lệnh cho Lexer bỏ qua toàn bộ phần đuôi còn lại của câu lệnh.
[!NOTE] Độ ưu tiên toán tử trong AST Trong chuẩn SQL, toán tử
ANDcó độ ưu tiên cao hơnOR(ANDbinds tighter thanOR). Nếu không dùng ký tự comment--để cắt bỏ phần sau, câu lệnhWHERE username = '' OR '1'='1' AND password = '...'sẽ được AST gom nhóm thànhWHERE (username = '') OR ('1'='1' AND password = '...'). Khi mật khẩu không khớp, nhánhANDtrả vềFALSE, khiến phépORcũng thất bại! Nhờ có--, nhánh kiểm tra mật khẩu bị cắt bỏ (prune) triệt để khỏi cây AST.
Kết quả là cây AST đã bị biến dạng hoàn toàn:
- Thay vì kiểm tra cặp username & password, điều kiện truy vấn chỉ còn:
username rỗngHOẶC1 bằng 1(luôn luônTRUE). - Database trả về bản ghi người dùng đầu tiên trong bảng (thường là tài khoản quản trị viên
admin). Kẻ tấn công đăng nhập thành công mà không cần mật khẩu!
Theo mô hình 01-threat-modeling-va-mo-hinh-stride, đây là sự kết hợp nguy hiểm giữa Tampering (sửa đổi logic câu lệnh) và Elevation of Privilege (chiếm quyền tài khoản quản trị).
Hãy thử kiểm tra hiểu biết của bạn về hành vi của bộ tách từ Lexer:
Giả sử câu truy vấn được viết: SELECT * FROM products WHERE id = USER_INPUT (id là kiểu số nguyên INTEGER). Hacker nhập giá trị USER_INPUT là: 1 OR 1=1. Kịch bản nào sau đây diễn ra trong Database?
3. Bản chất kỹ thuật của payload kinh điển ' OR '1'='1 là gì?
Trong các bài viết bảo mật cơ bản, bạn thường thấy payload ' OR '1'='1 hoặc ' OR 1=1 --. Hãy giải phẫu từng thành phần của payload này dưới góc độ cú pháp:
- Dấu nháy đơn đầu tiên (
'): Đóng chuỗi literal hiện thời do lập trình viên mở ra trong mã nguồn. - Toán tử
OR: Thêm một nhánh điều kiện logic mới vào mệnh đềWHERE. - Biểu thức
1=1(hoặc'1'='1'): Tạo ra một mệnh đề luôn luôn trả về giá trị chân lýTRUE. Do quy tắc của phép tínhOR, toàn bộ biểu thứcA OR TRUEsẽ luôn đánh giá làTRUEvới mọi bản ghi. - Ký hiệu bình luận (
--hoặc/*): Hủy bỏ (comment out) toàn bộ phần còn lại của câu lệnh SQL gốc (ví dụ mệnh đề kiểm tra password phía sau), giúp câu lệnh không bị lỗi cú pháp do thừa dấu nháy.
-- Vi du cau lenh bi vo hieu hoa phan sau boi dau comment
SELECT * FROM users WHERE username = 'admin' --' AND password = 'xxx'
4. Prepared Statements bảo vệ câu truy vấn ở tầng Lexer ra sao?
Tại sao các hàm lọc từ khóa như xóa chữ OR hay xóa dấu ' luôn thất bại? Tương tự như XSS, hacker có vô số cách để lách qua (viết hoa thường lộn xộn, dùng comment xen kẽ như /**/OR/**/).
Cách giải quyết triệt để và duy nhất được chấp nhận trong kỹ thuật phần mềm hiện đại là Prepared Statements (Parameterized Queries).
// Su dung Parameterized Query chuan muc voi thu vien pg (Node.js)
const text = 'SELECT * FROM users WHERE username = $1 AND password = $2';
const values = [req.body.username, req.body.password];
const res = await pool.query(text, values);
Cơ chế hoạt động bên trong:
- Pha 1: Biên dịch câu lệnh (Prepare):
Ứng dụng gửi câu lệnh có chứa các ký tự giữ chỗ (
$1,$2hoặc?) sang cơ sở dữ liệu. Cơ sở dữ liệu chạy Lexer và Parser để tạo ra cây AST hoàn chỉnh và đóng băng cấu trúc đó lại. Cây cú pháp này quy định cố định: Ở vị trí $1, đây bắt buộc phải là một giá trị dữ liệu (Data Literal). - Pha 2: Thực thi với tham số (Execute):
Ứng dụng gửi các giá trị biến
[username, password]sang. Lúc này, cơ sở dữ liệu hoàn toàn không chạy lại Lexer hay Parser. Dù giá trị của$1có chứa bao nhiêu dấu nháy đơn, bao nhiêu chữOR, hay bao nhiêu ký hiệu--, cơ sở dữ liệu vẫn coi nó là một chuỗi văn bản thuần túy không có khả năng thực thi!

Hình 3.4: Prepared Statement biên dịch cây AST từ trước và phân tách rạch ròi giữa code và data; dữ liệu truyền sau không bao giờ làm biến dạng cấu trúc lệnh.
5. Tổng kết: Tách biệt tuyệt đối dữ liệu và mã lệnh
Cội nguồn của mọi lỗ hổng Injection (SQL Injection, Command Injection, XSS) đều bắt nguồn từ một lỗi thiết kế duy nhất: Trộn lẫn dữ liệu không tin cậy vào luồng điều khiển mã lệnh.
Khi dữ liệu và mã lệnh được phân tách rõ ràng thông qua hai kênh độc lập (như cơ chế Prepared Statements), lỗ hổng SQL Injection sẽ biến mất hoàn toàn.
Bài tiếp theo: 04-idor-so-cap-va-rbac
- Q1Vì sao việc ghép chuỗi trực tiếp (String Concatenation) trong truy vấn database lại dẫn tới lỗ hổng SQL Injection?
- Q2Tại sao Prepared Statements (Parameterized Queries) lại ngăn chặn triệt để SQL Injection?
- Q3Trong payload kinh điển '' OR '1'='1', toán tử 'OR' đóng vai trò kỹ thuật gì?
- Q4Ký hiệu hai gạch ngang '--' (hoặc '#' trong MySQL) có ý nghĩa gì trong câu lệnh SQL bị tấn công?
- Q5Đối với trường dữ liệu kiểu số (ví dụ: WHERE id = 10), kẻ tấn công có cần dùng ký tự nháy đơn (') để khai thác SQL Injection không?
- Q6Tại sao phương pháp lọc từ khóa bằng Regex (Blacklist - ví dụ loại bỏ 'SELECT', 'UNION') lại bị coi là sai lầm nguy hiểm?
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
XSS Sơ cấp: Giải mã Reflected và Stored XSS
Bài tiếpIDOR Sơ cấp: Kiểm soát quyền truy cập mức đối tượng
Thảo luận & góp ý
Hỏi đáp về bài này