1. Tổng quan
Để hiểu và tự tin khi làm việc với SQL Injection (SQLi), bạn cần một số kiến thức nền tảng, biết cách cân bằng giữa việc đọc code và sử dụng tool, đồng thời theo lộ trình học tập hợp lý. Việc chỉ dùng tool scan thường không đủ; nền tảng SQL và code mới là gốc để bạn nhận diện, đánh giá và khắc phục lỗ hổng.
2. Kiến thức nền tảng cần có
-
SQL cơ bản
- Biết SELECT, INSERT, UPDATE, DELETE.
- Hiểu WHERE, AND, OR, UNION.
- Nhận ra việc nối chuỗi trực tiếp vào query là nguy hiểm.
-
Cách ứng dụng kết nối DB
- Ứng dụng nhận input từ form rồi chèn vào SQL.
- Hiểu được luồng dữ liệu để thấy “nếu người dùng nhập dữ liệu bất thường thì điều gì xảy ra”.
-
Bảo mật web cơ bản
- Nguyên lý input validation (kiểm tra dữ liệu đầu vào).
- Principle of least privilege (hạn chế quyền user DB).
- Lý do phải dùng hash mật khẩu.
3. Vai trò của code và tool
-
Biết code & SQL
- Giúp tự đọc/tự audit code để phát hiện nguy hiểm.
- Hiểu tại sao tool báo SQLi và cách fix triệt để.
- Ví dụ: nhìn thấy
"SELECT * FROM users WHERE id = '" + id + "'"
là nhận ra rủi ro.
-
Chỉ dùng tool scan (SQLmap, Burp Suite, OWASP ZAP)
- Có thể phát hiện lỗ hổng nếu tool dò ra.
-
Tuy nhiên:
- Có thể false positive/false negative.
- Nếu không biết SQL, sẽ khó đánh giá mức độ nghiêm trọng.
- Không biết SQL ⇒ khó sửa tận gốc.
→ Thực tế: tool chỉ là trợ thủ, kiến thức nền mới là cốt lõi.
4. Lộ trình học tập hợp lý
- Học SQL cơ bản (SELECT, WHERE, UNION).
- Hiểu cách code nối input vào SQL (PHP, Python, Node.js).
- Học best practices: prepared statements, ORM, hashing password.
- Làm lab nhỏ (DVWA, bWAPP, PortSwigger Labs).
- Sau đó dùng tool (SQLmap, Burp) để tự kiểm thử hệ thống.
5. Một số playground an toàn
-
Các web app dễ bị tấn công
- DVWA (Damn Vulnerable Web Application)
- bWAPP (Buggy Web Application)
- OWASP Juice Shop
-
Lab online
- PortSwigger Web Security Academy
- HackTheBox (HTB) – Starting Point
-
Tự dựng lab nhỏ
- Flask/Django (Python) hoặc PHP + MySQL.
- Cố tình viết code nối chuỗi vào query.
- Thử payload rồi fix bằng prepared statements để so sánh.
-
Công cụ hỗ trợ
- Burp Suite Community Edition
- SQLmap
- Postman
6. Ví dụ
Dưới đây là một bản demo Flask + SQLite để bypass đăng nhập bằng SQL injection và một phiên bản đã fix để so sánh.
⚠️ Chỉ dùng trong lab/local do bạn dựng. Không thử trên hệ thống không thuộc quyền của bạn.
6.1) Bản dễ dính SQLi — có thể bypass login
# app_vuln.py
from flask import Flask, request, render_template_string
import sqlite3
app = Flask(__name__)
DB = "lab.db"
PAGE = """
<!doctype html>
<title>Login (Vulnerable)</title>
<h2>Login (Vulnerable)</h2>
<form method="post">
<input name="username" placeholder="username">
<input name="password" placeholder="password" type="password">
<button type="submit">Login</button>
</form>
<pre>{{msg}}</pre>
"""
def init_db():
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("DROP TABLE IF EXISTS users")
cur.execute("""
CREATE TABLE users(
id INTEGER PRIMARY KEY,
username TEXT UNIQUE,
password TEXT
)
""")
# Mật khẩu lưu plain-text cho mục đích demo (KHÔNG làm vậy trong thực tế)
cur.execute("INSERT INTO users(username,password) VALUES('alice','alice123')")
cur.execute("INSERT INTO users(username,password) VALUES('bob','bob123')")
con.commit()
con.close()
@app.route("/init")
def init():
init_db()
return "DB initialized"
@app.route("/login", methods=["GET","POST"])
def login():
msg = "Nhập thử tài khoản/mật khẩu."
if request.method == "POST":
u = request.form.get("username","")
p = request.form.get("password","")
# ❌ LỖI: GHÉP CHUỖI TRỰC TIẾP VÀO SQL => DÍNH SQLi
query = f"SELECT id, username FROM users WHERE username = '{u}' AND password = '{p}'"
con = sqlite3.connect(DB)
cur = con.cursor()
try:
cur.execute(query)
row = cur.fetchone()
if row:
msg = f"✅ BYPASS/LOGIN THÀNH CÔNG: user={row[1]}\n\nQuery chạy:\n{query}"
else:
msg = f"❌ Sai tài khoản/mật khẩu\n\nQuery chạy:\n{query}"
except Exception as e:
msg = f"⚠️ Lỗi SQL: {e}\n\nQuery chạy:\n{query}"
finally:
con.close()
return render_template_string(PAGE, msg=msg)
if __name__ == "__main__":
app.run(debug=True)
Cách chạy & bypass (SQLite)
python app_vuln.py
# Mở http://127.0.0.1:5000/init (tạo DB)
# Mở http://127.0.0.1:5000/login
Trong form login, thử:
- Username:
' OR '1'='1' --
- Password: (gõ gì cũng được)
Giải thích: payload đóng dấu '
để thoát khỏi chuỗi, thêm điều kiện OR ‘1’=’1′ luôn đúng, rồi --
(kèm khoảng trắng) để comment phần còn lại (bao gồm điều kiện mật khẩu).
Câu query sẽ thành kiểu:
SELECT id, username FROM users
WHERE username = '' OR '1'='1' -- ' AND password = 'bất_kỳ'
→ Điều kiện WHERE luôn đúng ⇒ đăng nhập “thành công”.
Với MySQL, payload tương tự hoạt động (comment
--
hoặc#
).
6.2) Bản đã fix — chặn SQLi bằng parameterized queries
# app_safe.py
from flask import Flask, request, render_template_string
import sqlite3
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
DB = "lab_safe.db"
PAGE = """
<!doctype html>
<title>Login (Safe)</title>
<h2>Login (Safe, Parameterized)</h2>
<form method="post">
<input name="username" placeholder="username">
<input name="password" placeholder="password" type="password">
<button type="submit">Login</button>
</form>
<pre>{{msg}}</pre>
"""
def init_db():
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("DROP TABLE IF EXISTS users")
cur.execute("""
CREATE TABLE users(
id INTEGER PRIMARY KEY,
username TEXT UNIQUE,
password_hash TEXT
)
""")
cur.execute("INSERT INTO users(username,password_hash) VALUES(?,?)",
("alice", generate_password_hash("alice123")))
cur.execute("INSERT INTO users(username,password_hash) VALUES(?,?)",
("bob", generate_password_hash("bob123")))
con.commit()
con.close()
@app.route("/init")
def init():
init_db()
return "DB initialized (safe)"
@app.route("/login", methods=["GET","POST"])
def login():
msg = "Nhập thử tài khoản/mật khẩu."
if request.method == "POST":
u = request.form.get("username","")
p = request.form.get("password","")
con = sqlite3.connect(DB)
cur = con.cursor()
try:
# ✅ Dùng tham số hóa, không ghép chuỗi
cur.execute("SELECT password_hash FROM users WHERE username = ?", (u,))
row = cur.fetchone()
if row and check_password_hash(row[0], p):
msg = "✅ Login hợp lệ (đã phòng SQLi)"
else:
msg = "❌ Sai tài khoản/mật khẩu"
finally:
con.close()
return render_template_string(PAGE, msg=msg)
if __name__ == "__main__":
app.run(debug=True, port=5001)
Chạy:
python app_safe.py
# Mở http://127.0.0.1:5001/init
# Vào http://127.0.0.1:5001/login
# Thử lại payload SQLi ở trên -> KHÔNG bypass được
6.3) Tại sao chỗ payload ‘ OR ‘1’=’1′ — lại có thể bypass.
6.3.1) Ứng dụng đang ghép chuỗi để tạo SQL
Trong phiên bản lỗi, câu query là:
query = f"SELECT id, username FROM users WHERE username = '{u}' AND password = '{p}'"
{u}
là username bạn nhập từ form.{p}
là password bạn nhập từ form. → Cả hai được chèn thẳng vào câu SQL bằng dấu'...'
.
6.3.2) Khi nhập bình thường
Ví dụ nhập:
- username =
alice
- password =
alice123
Thì query thành:
SELECT id, username FROM users
WHERE username = 'alice' AND password = 'alice123';
- DB tìm đúng 1 dòng ⇒ đăng nhập hợp lệ.
6.3.3) Khi nhập payload SQLi để bypass
Bạn nhập:
- username =
' OR '1'='1' --
- password = (gì cũng được)
Thay vào query:
SELECT id, username FROM users
WHERE username = '' OR '1'='1' -- ' AND password = '______'
Giải thích luồng chạy:
-
username = ''
- Vì username bắt đầu bằng
'
, DB hiểu phần trước đó là chuỗi rỗng''
.
- Vì username bắt đầu bằng
-
OR '1'='1'
- Đây là một biểu thức đúng (so sánh chuỗi
'1'
với'1'
). - Trong logic:
A OR TRUE
⇒ luôn TRUE.
- Đây là một biểu thức đúng (so sánh chuỗi
-
--
(hai dấu gạch ngang + khoảng trắng)- Bắt đầu comment đến hết dòng.
- Mọi thứ phía sau (gồm
' AND password = '______'
) bị bỏ qua. - Nghĩa là điều kiện mật khẩu không còn hiệu lực.
→ Cả điều kiện WHERE
rút gọn về:
WHERE (username = '') OR (TRUE)
Mà OR TRUE
là luôn đúng ⇒ DB trả về ít nhất một dòng (thường là dòng đầu tiên).
Code thấy “có kết quả” ⇒ tưởng là đăng nhập thành công.
Lưu ý:
- MySQL:
--
(phải có khoảng trắng) hoặc#
đều là comment hợp lệ.- SQLite:
--
tới hết dòng là comment (thực tế đa số engine hiểu tốt--
có khoảng trắng).- Ngoài ra còn có comment kiểu
/* ... */
trong SQL chuẩn.
6.3.4) Vì sao prepared statements lại chặn được?
Với câu query dưới:
cur.execute("SELECT password_hash FROM users WHERE username = ?", (u,))
- Câu lệnh SQL gửi sang DB tách biệt với dữ liệu
u
. - Dữ liệu
u
(kể cả chuỗi' OR '1'='1' --
) được coi là giá trị chuỗi bình thường, không được thực thi như cú pháp SQL. - DB hiểu câu này là tìm
username
đúng chuỗi"' OR '1'='1' -- "
chính xác từng ký tự, nên không thể biến thànhOR 1=1
.
Tương tự với MySQL (pymysql / mysqlclient), PHP (PDO), Node (mysql2), Java (PreparedStatement)… tất cả đều có parameterized queries để chống SQLi.
6.3.5) Tóm tắt nhanh dễ nhớ
- Đóng dấu
'
để thoát khỏi chuỗi cũ. - Thêm
OR '1'='1'
để biến điều kiện thành luôn đúng. - Thêm comment (
--
hoặc#
) để vô hiệu hóa phần còn lại (đặc biệt là điều kiện password).
6.3.6) Một số biến thể thường gặp để bạn dễ nhận diện
- Nếu cột là kiểu số (không có
'...'
):id=1 OR 1=1
- Comment kiểu khác:
#
(MySQL),/* ... */
(chuẩn SQL). - Nếu app tự thêm dấu
'
cả hai bên, kẻ tấn công sẽ cố đóng chuỗi, chèn logic, rồi comment phần còn lại — đúng như ví dụ trên.
7. Checklist phòng thủ tối thiểu
- Parameterized queries / Prepared statements (mọi nơi có SQL).
- Hash mật khẩu (bcrypt/argon2/pbkdf2), không bao giờ lưu plain-text.
- Tối thiểu quyền DB (app user không có DROP/ALTER).
- Ẩn lỗi SQL khỏi người dùng.
- Validate input (độ dài, pattern).
- ORM (SQLAlchemy, Django ORM, v.v.) để giảm rủi ro ghép chuỗi.
8. Kết luận
- SQL Injection là lỗ hổng phổ biến và nguy hiểm, chỉ dùng tool scan không thể thay thế cho kiến thức nền.
- Cần nắm vững SQL cơ bản, hiểu luồng dữ liệu trong code, kết hợp với các nguyên tắc bảo mật web.
- Lộ trình tốt nhất: học SQL → hiểu code → học best practices → thực hành lab → dùng tool để kiểm thử.
Lời khuyên
- Đừng bỏ qua kiến thức SQL và code, vì đó là chìa khóa để fix tận gốc.
- Dành thời gian làm lab (DVWA, PortSwigger) thay vì chỉ đọc lý thuyết.
- Luôn thử nghiệm trong môi trường an toàn, tuyệt đối không test trên hệ thống thật.