CVE-2025-57833: Deep Dive into Django ORM FilteredRelation Alias Handling Vulnerability

CVE-2025-57833: Deep Dive into Django ORM FilteredRelation Alias Handling Vulnerability

04 September 2025

CVE-2025-57833: Deep Dive into Django ORM FilteredRelation Alias Handling Vulnerability

1. Introduction

Django ORM allows developers to interact with databases without writing raw SQL. However, this abstraction layer must carefully handle alias names when generating SQL, as mistakes here can lead to SQL injection vulnerabilities.

CVE-2025-57833 is a SQL injection vulnerability discovered in Django’s FilteredRelation combined with alias() and annotate(). The issue arises from unsafe handling of alias names.


2. How Django ORM Query Compilation Works

When you call a QuerySet like Order.objects.annotate(...), Django internally does the following:

  1. Build AST (Abstract Syntax Tree) – transforms expressions into Annotation objects.
  2. Resolve alias names – dictionary keys or arguments become SQL AS "alias".
  3. SQLCompiler – converts objects to raw SQL strings.
  4. Alias injection – alias names are injected directly into the generated SQL.

⚠️ Critical: Alias names come from dict keys. If they originate from user input, they directly influence the SQL output.


3. Normal Usage Example

# Adds payment.amount as a new alias column called total_amount
qs = Order.objects.annotate(total_amount=F("payment__amount"))
print(str(qs.query))

SQL output:

SELECT "myapp_order"."id",
       "myapp_payment"."amount" AS "total_amount"
FROM "myapp_order"
LEFT OUTER JOIN "myapp_payment"
  ON ("myapp_order"."id" = "myapp_payment"."order_id");

✅ Safe because alias name is a constant string.


4. FilteredRelation Example

qs = (
    Order.objects.alias(
        paid_payment=FilteredRelation("payment", condition=Q(payment__status="paid"))
    )
    .annotate(total_paid=F("paid_payment__amount"))
)
print(str(qs.query))

SQL output:

SELECT "myapp_order"."id",
       "myapp_payment"."amount" AS "total_paid"
FROM "myapp_order"
LEFT OUTER JOIN "myapp_payment"
  ON ("myapp_order"."id" = "myapp_payment"."order_id"
      AND "myapp_payment"."status" = 'paid');

🔎 Explanation:

  • paid_payment is the JOIN alias.
  • total_paid is the SELECT alias.
  • Two alias types exist, and Django processes them differently.
  • Vulnerability emerges because alias quoting in FilteredRelation bypasses Django’s escaping logic.

5. Dynamic Alias (Root Cause)

user_alias = "custom_label"   # Simulating user input
qs = Order.objects.annotate(**{user_alias: F("payment__amount")})
print(str(qs.query))

SQL output:

SELECT "myapp_order"."id",
       "myapp_payment"."amount" AS "custom_label"
FROM "myapp_order"
LEFT OUTER JOIN "myapp_payment"
  ON ("myapp_order"."id" = "myapp_payment"."order_id");

🔎 Explanation:

  • The dict key "custom_label" becomes SQL alias → AS "custom_label".
  • Normally Django quotes this safely.
  • In CVE-2025-57833, inside FilteredRelation, some characters bypass quoting (e.g., ", ), --).
  • Thus, injection is possible through alias names.

6. Technical Root Cause

  • Alias processing pipeline:
    1. get_extra_select() collects alias keys.
    2. resolve_ref() resolves field references.
    3. quote_name_unless_alias() decides whether to escape.
  • In FilteredRelation, aliases are mistakenly flagged as pre-resolved → no quoting applied.
  • User-controlled alias therefore leaks into SQL.

7. Risk Analysis

  • Privileges (PR:L): Low-privileged users can trigger.
  • Impact (C:H): High confidentiality impact (data leakage via JOIN manipulation).
  • Scope (S:C): Scope change possible (JOIN conditions altered).
  • Uniqueness: Unlike classical SQLi (in values), here injection occurs via alias names.

8. Safe PoC-like Demonstration

# 1) Constant alias (safe)
qs = Order.objects.annotate(payment_amount=F("payment__amount"))
print(str(qs.query))
# SQL: ... AS "payment_amount"

# 2) User alias (potentially unsafe)
user_alias = "custom_label"
qs = Order.objects.annotate(**{user_alias: F("payment__amount")})
print(str(qs.query))
# SQL: ... AS "custom_label"

# 3) With FilteredRelation (vulnerable context)
qs = (
    Order.objects.alias(
        paid_payment=FilteredRelation("payment", condition=Q(payment__status="paid"))
    )
    .annotate(total_paid=F("paid_payment__amount"))
)
print(str(qs.query))
# SQL: ... AS "total_paid" JOIN ... AND "status" = 'paid'

9. Correct vs Incorrect Usage Table

| Scenario | Python ORM Code | SQL Output | Status | |———-|—————–|————|——–| | ✅ Correct (Constant Alias) | python qs = Order.objects.annotate(payment_amount=F("payment__amount")) | sql ... AS "payment_amount" | Safe because alias is constant and quoted. | | ⚠️ Risky (User Input Alias) | python user_alias = "custom_label" qs = Order.objects.annotate(**{user_alias: F("payment__amount")}) | sql ... AS "custom_label" | Appears safe but alias comes from user input → risk. | | ❌ Vulnerable (FilteredRelation + User Alias) | python user_alias = "foo")--" qs = Order.objects.alias(**{user_alias: FilteredRelation("payment")}) | sql ... AS foo")-- | Vulnerable: quoting skipped, injection surface opened. |


10. Mitigation

  • Upgrade Django: 4.2.24, 5.1.12, 5.2.6 or later.
  • Never use user input as alias names.
  • Whitelist or regex-validate alias names.
  • Use constant aliases wherever possible.

11. Conclusion

CVE-2025-57833 is caused by improper handling of alias names in Django ORM’s FilteredRelation. Unlike classical SQL injection, the attack vector is through alias resolution rather than field values.

Safe demonstration shows how alias → SQL output mapping works and why alias quoting is crucial.
The fix in newer Django versions restores strict quoting and prevents injection via alias names.