参数化查询必须严格匹配数据库驱动占位符语法,否则失效;sqlite3用?或:name,pymysql和psycopg2只认%s,混用或字符串拼接将导致防护失效。

参数化查询必须匹配驱动占位符语法
不同数据库驱动对占位符的识别规则完全不同,用错就等于没防。比如 sqlite3 只认 ? 或 :name,传 %s 会被当成普通字符串;而 pymysql 和 psycopg2 只认 %s(注意不是 Python 的字符串格式化),传 ? 会直接报错或降级为拼接。
-
sqlite3:必须用?或命名参数:user_id,参数传元组或字典 -
pymysql:%s是唯一安全占位符,不能用?,也不能用f"WHERE id = {user_id}" -
psycopg2:只支持%s,且参数必须是元组((user_id,))或字典({"id": user_id}),传列表会报TypeError - 混用风险:写成
cursor.execute(f"SELECT * FROM t WHERE id = %s AND name = '{name}'")—— 后半段'{name}'已经是硬编码拼接,参数化失效
ORM 不等于自动免疫,raw() 和 extra() 是高危区
Django ORM 或 SQLAlchemy 查询集默认安全,但一旦调用原始 SQL 接口,防护就退回原点。这些地方最容易忽略:
-
raw()(Django)和text()(SQLAlchemy):必须把所有变量通过params=或第二参数传入,不能塞进 SQL 字符串里 -
extra()(Django):字段名、表别名若来自用户输入,需白名单校验,例如request.GET.get("order_by")必须限定在["id", "created_at"]内 -
filter(**{field: value}):如果field是用户可控的,比如request.GET.get("field"),攻击者可传__dict__或is_staff等敏感字段名 - SQLAlchemy 的
select().where(text("...")):只要text()内部含变量拼接,就等同于裸写 SQL
表名、字段名、ORDER BY、LIMIT 无法参数化
参数化只保护值(value),不保护结构(structure)。数据库驱动根本不允许 ORDER BY ? 或 SELECT ? FROM ? 这类写法——语法错误,不是安全机制。
- 动态表名:用映射字典硬编码,如
{"user": "users_v2", "log": "logs_2026"},用户输入仅作键查表,不在 SQL 中拼接 - 排序字段:只允许预设白名单,
allowed_sort = {"id", "name", "updated_at"},不在其中则 fallback 到默认值 - LIMIT 数量:必须转为整数并截断,
limit = max(1, min(100, int(request.args.get("limit", "20")))),再拼到 SQL 里(前提是来源可信) - IN 列表:不能传元组进
WHERE id IN %s,得按实际长度生成对应数量的%s占位符,再展开参数
错误提示和权限配置常被忽略
生产环境暴露详细数据库错误(比如 ProgrammingError: (1064, "You have an error in your SQL syntax..."))等于给攻击者递地图。最小权限配置不到位,一次注入就能拖库。
立即学习“Python免费学习笔记(深入)”;
- 关闭调试模式:Django 设为
DEBUG=False,Flask 关闭PROPAGATE_EXCEPTIONS,用通用错误页替代原生 DB 错误 - 数据库账号权限:只授予必要表的
SELECT、INSERT,禁用DROP、CREATE、EXECUTE、LOAD DATA - 连接池设置:避免长连接复用导致错误信息缓存泄露,尤其在异步框架(如 FastAPI + asyncpg)中要注意连接隔离
- 日志脱敏:记录 SQL 时过滤掉参数值,或只记模板如
"SELECT * FROM users WHERE email = ?"
真正卡住 SQL 注入的,从来不是“用了 ORM”或“写了 execute()”,而是每处动态拼接前是否问了一句:这东西是数据,还是结构?如果是后者,就得靠白名单、类型强转、范围限制来兜底——没有银弹,只有层层设防。


















