django orm默认防sql注入,但仅保护“值”不保护“结构”;字段名、表名、排序子句等动态结构元素必须白名单校验,否则仍存在注入风险。

Django ORM 默认防 SQL 注入,但仅限于“值”被参数化;一旦你让字段名、表名、ORDER BY 子句或 raw() 中的 SQL 结构部分由用户控制,漏洞立刻出现——这不是配置问题,而是用法边界没划清。
filter() 和 get() 为什么通常安全?
Django 在底层把 filter(username=request.GET.get('u')) 编译成参数化查询(如 WHERE username = %s),用户输入只进参数元组,不进 SQL 字符串。哪怕 request.GET.get('u') 是 "admin' OR 1=1 --",也只会查一个叫这个字符串的用户名。
- 安全前提是字段名硬编码:
User.objects.filter(username=...)✅,User.objects.filter(**{field_name: value})❌(field_name 来自请求) -
Q(name__icontains=search_term)安全,但Q(**{f"{field}__icontains": search_term})危险——键名不参数化 - 类型转换不影响安全性:
MyModel.objects.get(id=int(request.GET.get('id', 0)))仍走参数绑定
raw() 和 extra() 怎么写才不踩坑?
这两个方法完全跳过 ORM 查询编译器,直接拼 SQL 字符串。哪怕只拼一个单引号,就等于交出数据库钥匙。
- 危险写法:
.raw(f"SELECT * FROM users WHERE name = '{name}'")、.extra(where=["status = '{}'".format(user_input)]) - 正确写法:
.raw("SELECT * FROM users WHERE name = %s", params=[name]),params必须是列表或元组 -
extra(tables=["user_profile"])中的表名若来自用户输入,必须白名单校验,不能直接代入 -
extra(select={'score': "COALESCE({}, 0)".format(col)}是高危模式,col必须先判断是否在['age', 'score', 'level']中
动态字段、排序、聚合怎么控制?
ORM 不校验字段是否存在、是否可公开、是否属于当前模型——它只保证“值”安全。结构控制全靠你手动兜底。
-
order_by(request.GET.get('sort')):必须白名单,例如if sort not in ['id', '-created_at', 'email']才放行 -
values(*user_fields):先过滤user_fields = [f for f in user_fields if f in ['title', 'author__name']] -
annotate(score=Func(request.GET.get('col'), function='COALESCE')):request.GET.get('col')要映射到可信字段,或用getattr(MyModel, col, None)确认存在且非私有 -
Q(**{field_name: value}):同理,field_name必须来自白名单,不能直传
为什么 CVE-2025-64459 说明“默认安全”不等于“绝对安全”?
该漏洞出现在 filter() 的 _connector 参数被恶意字典污染时,证明 Django 并非对所有输入路径都做结构校验。它只保障标准调用路径(字段名硬编码 + 值传参)的安全,不保障开发者自己构造的动态逻辑。
- 别依赖“ORM 就是安全的”这种笼统认知,重点盯住字段名、表名、函数名、排序方向这些结构元素的来源
- 所有从 request、querystring、body 解析出来的字符串,只要可能进 SQL 结构位置,就必须过白名单或存在性检查
- 最易忽略的是
values_list('user_input_field')或only('user_input_field')——字段名没进 where,但进了 SELECT,一样能触发非法 JOIN 或权限绕过











