DjangoORM仅对值参数化,字段名等结构参数需白名单校验。filter()动态字段名、raw()和extra()参数化、RawSQL中LIKE手动转义及ORDERBY等结构字段均为注入风险点,需严格规避用户直接控制。
许多开发者容易忽略一个核心判断:Django ORM在防止SQL注入方面,只保护了“值”这一层。字段名、表名等结构参数,它根本不管。市面上流行的“引入安全库”建议,反而可能让人忽视真正的关键控制点。
与其依赖第三方库,不如先把Django ORM自带的防御机制吃透——参数化查询是默认武器,配合白名单校验,覆盖绝大多数场景完全够用。
长期稳定更新的攒劲资源: >>>点此立即查看<<<

很多人的代码写成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安全边界内的写法。
这两个方法是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的机制有点特殊:它只对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=对它们来说形同虚设。
哪怕是代码里100%没拼过任何SQL字符串,只要写了order_by(request.GET.get('o'))而没有做白名单校验,攻击者就能通过这个入口触发全表扫描,或者暴露索引信息。这才是Django防SQL注入这场战役中,真正需要警惕的“主力对手”。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述