Django ORM防SQL注入:打破三个常见误解,掌握正确防御方法
许多开发者容易忽略一个关键事实:Django ORM在防御SQL注入时,仅对“值”进行保护,而字段名、表名等结构参数完全不受保护。市面上所谓的“引入安全库”建议,反而可能分散注意力,让人忽略真正的控制要点。
因此,与其盲目依赖第三方库,不如深入掌握Django ORM内置的防御机制——参数化查询作为默认武器,配合白名单校验,足以应对绝大多数场景。

filter()中动态字段名:一个易被忽视的SQL注入“后门”
许多开发者写类似代码:filter(**{request.GET.get('field'): value}),并以为ORM会完整过滤整个表达式。但事实并非如此——Django的绑定机制仅对值有效,键名则完全不受校验。
设想一下,攻击者传入 is_staff__gt、_meta.model_name 或 password__contains 等参数,后果如何?轻则泄露敏感字段,重则导致越权查询。这并非危言耸听。
正确的做法其实很简单:首先进行白名单检查,例如 if field_name not in ['title', 'author__name', 'status'],然后使用 getattr(MyModel, field_name, None) 确认字段真实存在且非 property。更重要的是,切勿使用 Q("username = '{}'".format(u)) 拼接字符串——应改用 Q(**{field_name + '__icontains': u}),这才是符合ORM安全边界的方式。
raw()和extra():原生SQL入口,SQL注入高发区域
这两个方法是ORM中仅允许编写原生SQL的接口,也是最容易引发安全问题之处。它们不继承filter()的安全逻辑,在其中拼接字符串无异于自找麻烦。
以 raw() 为例:安全写法为 User.objects.raw("SELECT * FROM auth_user WHERE username = %s", params=[username])。需注意两个细节:params 必须是列表或元组;占位符仅能使用 %s,不可使用 {} 或 %(name)s——因为SQLite不支持命名参数。
至于 extra():许多人认为 extra(where=["status = %s"], params=[u]) 是安全的,但实际情况是——extra() 的 params 参数在Django内部会被忽略。因此,where 字符串中绝不能出现用户输入。若需实现动态条件,最稳妥的做法是使用 Q 组合 + filter() 提前替代,而非将逻辑硬塞入 extra()。
RawSQL与annotate()中的LIKE:手动转义不可或缺
RawSQL 的机制较为特殊:它仅对 params 对应的占位符进行参数化,而整个表达式本身不会自动转义。这意味着,一旦未对 % 和 _ 进行转义,它们便会成为SQL通配符,轻则引发逻辑漏洞,重则拖垮数据库性能。
错误示例:annotate(score=RawSQL(f"CASE WHEN title LIKE '%{search}%' THEN 1 ELSE 0 END"))。这种写法相当于直接将用户输入嵌入SQL模板。
正确写法应为:RawSQL("CASE WHEN title LIKE %s THEN 1 ELSE 0 END", params=[f"%{search}%"])。但这仍不够稳妥——更推荐采用ORM原生表达式:Case(When(title__icontains=search, then=1), output_field=IntegerField())。原生表达式会自动处理转义,同时还能利用数据库索引,一举两得。
若确实必须使用原生LIKE,则应调用 connection.ops.prep_for_like_query(search) 进行手动转义。
最易被忽视的陷阱:结构参数
最后提及一个最易被忽略的“隐蔽角落”:字段名、表名、ORDER BY、GROUP BY、函数名等结构参数。参数绑定机制对这些参数完全无效——params= 对于它们形同虚设。
即便代码中从未拼接任何SQL字符串,只要使用了 order_by(request.GET.get('o')) 而未进行白名单校验,攻击者便可借此入口触发全表扫描或泄露索引信息。这才是Django防御SQL注入战役中,真正需要警惕的“主要威胁”。
