信任边界崩塌:详解Django ORM如何因内部控制参数暴露导致SQL注入(CVE-2025-64459)
介绍Django凭借其“全功能”特性和成熟的对象关系映射(ORM)系统,从初创企业的SaaS后端到财富500强级平台均有应用。而CVE-2025-64459漏洞正是出现在ORM层:当内部查询控制参数泄 2025-11-20 12:30:42 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

介绍

Django凭借其“全功能”特性和成熟的对象关系映射(ORM)系统,从初创企业的SaaS后端到财富500强级平台均有应用。而CVE-2025-64459漏洞正是出现在ORM层:当内部查询控制参数泄露到查询构造过程中时,会触发SQL注入漏洞。具体而言,若应用程序将用户可控的字典展开传递至QuerySet.filter()、exclude()、get()或Q()调用时,攻击者可通过注入_connector关键字改变ORM向数据库发送的逻辑语句。在实际应用中,这种漏洞常见于未认证接口的通用筛选模块,可能导致数据窃取和权限绕过等严重后果。Django官方根据安全政策将其评定为高危漏洞,并于2025年11月5日发布修复版本。

漏洞概述

Django常规查询构建机制

在正常条件下,Django ORM 会以高度受控的方式将关键字参数转换为 SQL:

# Intended, safe-style usage
User.objects.filter(
    is_active=True,
    email__icontains="example.com",
)

每个关键字都对应一个字段查询。Django 会负责对值进行转义,并构建如下的 WHERE 子句:

WHERE "user"."is_active" = TRUE
  AND "user"."email" ILIKE '%example.com%'

针对更复杂的逻辑,开发人员会使用Q对象来构建布尔表达式树:

from django.db.models import Q
qs = User.objects.filter(
    Q(is_active=True) & (Q(role="admin") | Q(role="staff"))
)

在内部实现中,Q对象维护着一个子节点列表和一个连接器,该连接器指示Django使用AND、OR还是XOR来连接这些子节点。此外还存在一个_negated标志位,用于对子树进行逻辑取反。
这些控制参数——_connector 与 _negated——属于内部调控机制,不应由终端用户操控。

问题根源:_connector 与字典扩展的叠加使用

当应用程序同时涉及以下两种操作时,问题便会显现:

  1. 用户输入数据的字典展开

  2. Django允许将 _connector(及 _negated)作为关键字参数传入 Q 对象和 QuerySet 方法

在实际的Django应用开发中,"动态筛选"是一种非常常见的编程模式:

# Risky pattern if request.GET is not validated
def search_users(request):
    filters = request.GET.dict()      # user-controlled
    users = User.objects.filter(**filters)
    return users

若用户提交如下请求:

[email protected]&_connector=...

那么在补丁发布前,Django会接受_connector作为查询树内部条件组合方式的控制参数。这意味着用户输入不再仅仅是数据——它开始能够影响生成的SQL语句结构。

即使直接通过原始字典构建Q对象,同样会触发此问题:

from django.db.models import Q
def post_list(request):
    query_params = dict(request.GET.items())  # user-controlled
    q_filter = Q(**query_params)              # also dangerous
    posts = Post.objects.filter(q_filter)
    ...

通过精心构造的_connector值,攻击者能够强制查询树中的部分节点以异常方式连接,甚至在某些情况下将SQL代码片段注入连接器本身。

Django在补丁中的核心修改

Django的修复方案在两个层面实施:

  1. QuerySet 层级验证

在 django/db/models/query.py 中,Django 现在维护了一个被禁关键字参数集合,并会提前拦截这些参数:

PROHIBITED_FILTER_KWARGS = frozenset(["_connector", "_negated"])
def _filter_or_exclude_inplace(self, negate, args, kwargs):
    if invalid_kwargs := PR

文章来源: https://www.freebuf.com/articles/vuls/458282.html
如有侵权请联系:admin#unsafe.sh