所有用户可控数据必须使用参数化查询,动态表名/列名需白名单校验,禁用字符串拼接SQL,生产环境关闭详细错误回显,优先选用官方推荐数据库驱动。

输入点:所有用户可控数据都得进参数化查询,别信任何“看起来安全”的拼接
SQL注入的起点永远是用户能改的地方——表单、URL参数、请求头、Cookie、甚至文件名。只要这个值最后进了 SQL 语句,又没走参数化,就是缺口。
-
request.args.get('id')、request.form['username']、json.loads(request.data).get('query')这些都不是“可信输入”,和手写字符串没区别 - 字符串格式化(
f"SELECT * FROM users WHERE id = {user_id}")、%拼接、.format()全部高危,哪怕加了int()强转也拦不住1 OR 1=1这种绕过 - ORM 的
.filter(name__contains=xxx)是安全的,但.extra(where=[f"status = '{status}'"])就是裸奔
用 sqlite3 举例:必须用 cursor.execute("SELECT * FROM logs WHERE level = ?", (level,)),而不是 execute(f"WHERE level = '{level}'")。
执行点:预编译语句不是万能的,动态表名/列名/ORDER BY 仍需白名单校验
参数化只防“值”,不防“结构”。SELECT * FROM ? 或 ORDER BY ? 在绝大多数驱动里直接报错,因为占位符不能替代标识符。
- 表名、列名、排序方向(
ASC/DESC)、UNION分支这类语法成分,必须从预设集合里选 - 别用正则“过滤掉危险字符”——
re.sub(r'[^a-zA-Z0-9_]', '', user_table)挡不住users--或宽字节绕过 - 更稳妥的是映射:把前端传的
sort=created_at_desc映射成("created_at", "DESC"),再拼进 SQL
常见翻车场景:ORDER BY {user_input} 看似只控制排序,但配合 UNION SELECT 可导致全表泄露。
输出点:前端渲染时别把数据库原始错误堆栈当提示返回给用户
psycopg2.errors.UndefinedTable: relation "usersx" does not exist 这类信息暴露了表结构;sqlite3.OperationalError: near "OR": syntax error 直接告诉你注入点在哪。
- 开发环境可以开 debug,生产环境必须关掉详细错误页,统一返回
500 Internal Server Error - 日志里记全量错误(含 SQL 和参数),但响应体里只给模糊提示,比如
{"error": "Invalid request"} - 千万别把
str(e)直接塞进 JSON 响应,尤其 ORM 报错常带 SQL 片段
这点常被忽略:防御做得再严,错误回显一开,相当于把探测器免费送给攻击者。
为什么 prepared statement 不等于绝对安全?看驱动实现细节
不是所有叫“参数化”的东西都真正隔离了执行计划。某些旧版 MySQL 驱动(如早期 MySQLdb)在开启 use_unicode=False 时,会把参数先编码再拼接;PostgreSQL 的 pg8000 对数组参数处理不当也可能触发二次解析。
- 优先用官方推荐驱动:
psycopg2(PostgreSQL)、pymysql(MySQL)、sqlite3(内置,够用) - 检查是否真用了 server-side prepare:MySQL 要确认
cursor.execute()调用的是mysql_stmt_prepare,不是客户端模拟 - 如果用连接池(如
SQLAlchemy),确保pool_pre_ping=True,避免连接复用时预编译失效
最麻烦的是嵌套场景:ORM 生成的 SQL 本身安全,但你手动拼了子查询,再喂给 ORM 执行——那个子查询就又回到输入点问题。
事情说清了就结束。

















