参数化查询是防止SQL注入的唯一可靠机制,因它强制分离SQL模板与数据值,使数据库不解析用户输入;拼接字符串、过滤不全、前端校验、ORM不当用法及动态标识符均无法替代。

因为不遵守参数化查询规范,等于主动把数据库的执行权交给任意客户端输入——不是“可能被攻破”,而是“只要输入可控,就必然沦陷”。
拼接SQL字符串时,数据库根本分不清哪是代码哪是数据
当你写 cursor.execute("SELECT * FROM users WHERE name = '" + user_input + "'"),数据库收到的是一整条已拼好的字符串。它不会去“识别”user_input里有没有' OR 1=1 --,只会按语法解析执行。攻击者输一个' OR '1'='1,语句就变成:
SELECT * FROM users WHERE name = '' OR '1'='1'
结果是全表扫描、越权读取,甚至配合;还能执行多语句(取决于驱动是否启用multi_statements)。
- 哪怕你对输入做了
re.sub(r"[^a-zA-Z0-9]", "", user_input),漏掉;、--、/*或Unicode变体就前功尽弃 - base64编码、JWT签名、前端校验——全在服务端之外,对后端SQL构造毫无约束力
- ORM的
.filter(name__contains=xxx)是安全的,但一旦用.extra(where=[f"name LIKE '%{xxx}%'"])或text()拼接,立刻退化为裸拼
参数化查询不是“更推荐”,而是驱动层强制隔离的唯一机制
主流驱动(psycopg2、sqlite3、mysql2、pg、PDO、SqlCommand)都把参数和SQL模板分开传给数据库协议。值不参与语法解析,只作为独立数据绑定到预编译语句占位符上。
-
sqlite3只认?或:name,用%s会直接报错 -
psycopg2只认%s,写?会被当字面量,查不到数据还不报错 - 参数必须是
tuple(单值也要写(value,))或dict,传list或str会触发类型错误或静默失败 -
IN子句不支持数组自动展开:WHERE id IN %s传[1,2,3]只匹配第一个值;得手动生成?, ?, ?再execute(sql, tuple(ids))
动态标识符(表名、字段名、ORDER BY字段)不能靠参数化保护
参数化只防“值注入”,不防“结构注入”。如果业务需要根据Cookie里的tenant_id查不同表,写"SELECT * FROM " + tenant_table + " WHERE ..."照样崩。
- 必须用白名单映射:
{"us": "users_us", "eu": "users_eu"},不在列表里直接拒掉 -
ORDER BY字段若来自请求参数,得限定为["created_at", "updated_at", "score"]等固定枚举 - 分页的
LIMIT和OFFSET虽是数字,但也不能用f"LIMIT {limit}",要转成整型后校验范围再拼(如max(1, min(limit, 100)))
最常被忽略的点:参数化解决的是“值怎么进SQL”,但“SQL本身是否合理”还得靠其他手段——比如无条件UPDATE、没索引的LIKE '%xxx%'、跨库未限定库名的查询,这些都不触发注入,却一样导致数据错乱或性能雪崩。

















