3. SQL 注入防御
漏洞代码:字符串拼接的登录查询
下面这段 Python + SQLite 登录逻辑是典型的注入入口。它把用户输入直接拼进 SQL 文本,数据库无法区分“数据”和“代码”,攻击者只要让拼接结果改变 SQL 语义,就能绕过密码校验。
import sqlite3
def login_unsafe(username, password):
conn = sqlite3.connect("app.db")
cur = conn.cursor()
sql = "SELECT id, role FROM users WHERE username = '" + username + \
"' AND password = '" + password + "'"
cur.execute(sql) # 危险:输入被当作 SQL 语法解析
row = cur.fetchone()
conn.close()
return row # 非 None 即登录成功
关键问题在 cur.execute(sql):传入的是已经拼好的完整 SQL 字符串,参数值参与了语法解析。只要 username 或 password 中含单引号、OR、注释符等,SQL 结构就会被改写。
注入原理:数据如何变成代码
以输入 username = admin' --、password = 任意值 为例,拼接后的 SQL 变为:
SELECT id, role FROM users WHERE username = 'admin' --' AND password = '任意值'
-- 是 SQL 行注释,其后的密码条件被整段注释掉,查询退化为“只要 username 为 admin 就返回记录”。若 username 输入 ' OR '1'='1,则条件恒真,可能返回任意一行用户记录。两种 payload 的共同点是:它们都改变了 WHERE 子句的逻辑结构,而不是作为普通字符串值参与比较。
改造:用占位符绑定参数
SQLite 的 ? 是参数占位符。数据库先编译带占位符的语句结构,再把参数值作为数据填入,因此参数内容永远不会被当作 SQL 语法。改造后的登录函数如下:
import sqlite3
def login_safe(username, password):
conn = sqlite3.connect("app.db")
cur = conn.cursor()
sql = "SELECT id, role FROM users WHERE username = ? AND password = ?"
cur.execute(sql, (username, password)) # 参数以元组传入,不参与拼接
row = cur.fetchone()
conn.close()
return row
注意两点:一是 SQL 字符串中不再出现任何用户输入;二是参数必须通过 execute 的第二个参数传入,而不是用 %、format 或 + 拼进 SQL 文本。若使用命名占位符,可写成 WHERE username = :u AND password = :p,并传入 {"u": username, "p": password}。
构造 payload 验证攻击失效
准备一个测试库,插入一条已知用户记录,然后用同一组注入 payload 分别调用 login_unsafe 和 login_safe,对比返回结果。
def setup():
conn = sqlite3.connect("app.db")
conn.execute("DROP TABLE IF EXISTS users")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, role TEXT)")
conn.execute("INSERT INTO users (username, password, role) VALUES (?, ?, ?)",
("admin", "S3cret!", "admin"))
conn.commit()
conn.close()
payloads = [
("admin' --", "whatever"),
("' OR '1'='1", "' OR '1'='1"),
("admin' OR 1=1 --", "x"),
]
setup()
for u, p in payloads:
unsafe = login_unsafe(u, p)
safe = login_safe(u, p)
print(f"payload={u!r:22} unsafe={unsafe} safe={safe}")
预期输出(示例):
payload="admin' --" unsafe=(1, 'admin') safe=None
payload="' OR '1'='1" unsafe=(1, 'admin') safe=None
payload="admin' OR 1=1 --" unsafe=(1, 'admin') safe=None
判读标准:unsafe 返回了记录,说明拼接版本被绕过;safe 返回 None,说明参数化版本把 payload 当作普通字符串比较,未匹配到任何用户,注入失效。若 safe 也返回了记录,应检查是否误用了字符串格式化、是否把参数拼进了 SQL 文本,或是否在别处仍存在拼接查询。