raw()必须用params=传参且仅支持%s占位符,不可字符串拼接;extra()的where等参数须通过params绑定;rawsql混用用户输入需手动参数化;结构化参数如字段名须白名单校验。

raw() 必须用 params= 且只认 %s
Django 的 raw() 完全跳过 ORM 编译器,SQL 字符串由你全权负责。只要出现 f"WHERE name = '{name}'" 或 "%s" % user_input,就等于把数据库钥匙交出去。
它只支持位置占位符 %s,所有后端(PostgreSQL/MySQL/SQLite)都兼容;命名参数如 %(name)s 在 SQLite 下直接报错。
-
params必须是列表或元组,不能是字典(除非你 100% 只跑 PostgreSQL 且确认驱动支持) - 错误写法:
User.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'") - 正确写法:
User.objects.raw("SELECT * FROM auth_user WHERE username = %s", params=[username]) - 占位符顺序必须和
params列表索引严格一致,错一位就可能查错数据
extra() 的 where 参数不自动转义,必须走 params
extra() 看似像 filter(),但它内部的 where、tables、joins 都是原始字符串拼接点,不经过任何参数化处理。
-
extra(where=["status = %s"], params=[user_status])是唯一安全路径 -
extra(where=[f"status = '{user_status}'"])和裸写 SQL 没区别,直接触发注入 -
extra()的params参数仅对where中的%s生效,其他地方(如tables)不支持params绑定 - 真需要动态条件,优先改用
Q()组合,或提前定义字段白名单映射
RawSQL 混用户输入必须手动参数化,LIKE 还要额外转义
RawSQL 用在 annotate() 或 filter() 中时,它本身不继承外层 QuerySet 的参数化机制,params= 只绑定它自己 SQL 内的占位符。
- 错误:
annotate(score=RawSQL(f"CASE WHEN title LIKE '%{search}%' THEN 1 ELSE 0 END")) - 正确:
RawSQL("CASE WHEN title LIKE %s THEN 1 ELSE 0 END", params=[f"%{search}%"]) - 用户输入中的
%和_会被数据库当通配符执行,不是注入但属逻辑漏洞;需用connection.ops.prep_for_like_query(search)手动转义,或改用title__icontains=search - 更稳妥方案:
Case(When(title__icontains=search, then=1), output_field=IntegerField())—— ORM 原生表达式自动处理转义和索引优化
结构参数永远不进 params,必须白名单校验
字段名、表名、ORDER BY、GROUP BY、函数名这些都不走参数绑定。params= 粘不住它们,哪怕你 100% 没拼过 SQL 字符串,只要 order_by(request.GET.get('o')) 没白名单,就可能被用来触发全表扫描或敏感字段泄露。
-
filter(**{field_name: value})动态传键名不校验,攻击者可传is_staff__gt、_meta.model_name甚至password__contains - 必须白名单检查:
if field_name not in ['title', 'author__name', 'status']:再抛异常 - 用
getattr(MyModel, field_name, None)确认字段真实存在且非 property -
values()、order_by()、annotate()中所有字符串参数,只要来源不可信,都得走白名单
最常被忽略的是:ORM 的安全机制只覆盖“值”,不覆盖 SQL 结构。结构参数没白名单,再严格的 params 也救不了。











