← Back to Case Studies
CVE-2024-42005 CVSS 9.3 Critical CWE-89 2-Line Fix

Django JSONField SQL Injection — Unvalidated Column Aliases in QuerySet.values()

Django 4.2/5.0 allowed SQL injection via crafted JSON object key paths passed as column aliases to QuerySet.values(). The fix adds just 2 lines — easy to miss in review.

CVSS 9.3 / 10.0
CWE CWE-89 (SQL Injection)
Disclosed Aug 6, 2024
Affected Django 4.2 < 4.2.15, 5.0 < 5.0.8
Discovery Eyal Gabay (EyalSec) via HackerOne #2646493
Repositories django/django
// what happened
  • Affected: Django 4.2 < 4.2.15, 5.0 < 5.0.8 — any model using JSONField with .values() or .values_list()
  • Root cause: QuerySet.values() accepted raw JSON object key paths as SQL column aliases without validation. check_alias() existed in the same file but was never called on values() inputs.
  • Attack: Crafted key like 'data__"injected" FROM "users"; --' breaks out of the alias and injects SQL. Unauthenticated attacker reads any table the DB user can access.
  • Fix: 2 lines — a for field in fields: self.check_alias(field) loop added to set_values() in django/db/models/sql/query.py

Base Score
9.3 Critical
Attack Vector (AV)
Network — exploitable over HTTP
Attack Complexity (AC)
Low — no special conditions needed
Privileges Required (PR)
None — unauthenticated attacker
User Interaction (UI)
None — no victim action required
Scope (S)
Changed — affects resources beyond the vulnerable component
Confidentiality (C)
High — full database read access
Integrity (I)
Low — read-only impact in most configurations
Availability (A)
None
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N

django/db/models/sql/query.py — set_values() method Django 4.2.14 / 5.0.7 — No validation on JSON key paths used as column aliases def set_values(self, fields): self.has_select_fields = True if fields: # ← MISSING: no check_alias() call here field_names = [] extra_names = [] annotation_names = []
// annotation
check_alias() already existed in this same file — it blocked quotes, semicolons, whitespace, and SQL comments via FORBIDDEN_ALIAS_PATTERN. But it was only called in add_annotation(), not in set_values(). JSON key paths like data__someKey passed to values() became SQL column aliases without validation.
django/db/models/sql/query.py — check_alias() already exists This validator existed — but was never called on *args to values() def check_alias(self, alias): if FORBIDDEN_ALIAS_PATTERN.search(alias): raise ValueError( "Column aliases cannot contain whitespace characters, quotation marks, " "semicolons, or SQL comments." )
// the fix: 2 lines
for field in fields: self.check_alias(field) — that's all it took. Add this loop at the start of set_values() and every field passed to values()/values_list() is validated before it becomes a SQL column alias.
django/db/models/sql/query.py — set_values() — THE FIX Django 4.2.15 / 5.0.8 — 2 lines added to validate column aliases def set_values(self, fields): self.has_select_fields = True if fields: for field in fields: self.check_alias(field) # ← THE FIX: 2 lines field_names = [] extra_names = [] annotation_names = []
Fix commit: github.com/django/django/commit/f4af67b9b41e0f4c117a8741da3abbd1c869ab28
// root cause
QuerySet.values() and values_list() accept positional string arguments that become SQL column aliases in the generated SELECT clause. On models with JSONField, Django uses JSON object key paths (e.g., data__someKey) to derive these aliases. The FORBIDDEN_ALIAS_PATTERN already blocked quotes, semicolons, whitespace, and SQL comments — but this validation was only applied in add_annotation(), not in set_values(). Any JSON key containing ", ;, --, or whitespace could break out of the alias and inject SQL. The fix is a 2-line loop that calls the already-existing check_alias() on every field passed to set_values().

How an unauthenticated attacker reads any table in the database
1
Attacker controls JSON stored in a JSONField — via API input, file upload, or direct database write.
2
Attacker passes a crafted key to .values(): Model.objects.values('data__"injected_name" FROM "users"; --')
3
Django generates SQL like: SELECT "data__"injected_name" FROM "users"; --" FROM app_model
4
The -- comments out the rest of the query. The attacker controls which columns are returned — or stacks additional SQL statements on DBs that allow it.
5
Result: unauthenticated read of any table the database user can access. With stacked queries: UPDATE/DELETE/INSERT also possible.

models.py — Django model with JSONField + .values() call class EventLog(models.Model): metadata = models.JSONField(default=dict) created_at = models.DateTimeField(auto_now_add=True) def get_injected_field(self): # Values() call accepts any string as a column alias # No validation on JSON key paths — attacker can inject SQL return EventLog.objects.values('metadata__"injected_name" FROM "users"; --')
PullLight synthetic fix — add check_alias() loop to set_values() # PullLight would flag: set_values() never calls check_alias() on its inputs # Fix: for field in fields: self.check_alias(field) in django/db/models/sql/query.py # Also: validate that JSON key paths used as values() args don't contain # quotes, semicolons, or SQL comments before constructing the query def get_injected_field(self): # After the Django fix, this is protected server-side # The real vulnerability was in the ORM internals — not this user code return EventLog.objects.values('metadata__someKey')
🔴 PullLight — High Severity Finding
[HIGH] Unvalidated SQL column alias in set_values() — django/db/models/sql/query.py

QuerySet.values() / values_list() accepts positional string arguments that become SQL column aliases. On JSONField models, JSON object keys are used to derive these aliases — but no validation was applied.

self.check_alias() exists in this file and blocks quotes, semicolons, and SQL comments. But it was never called on the *args passed to set_values().

Exploit path: Model.objects.values('data__"injected" FROM "users"; --') → SQL: SELECT "data__"injected" FROM "users"; --" FROM table"--" comments out rest of query → attacker controls column selection.

This is CWE-89 (SQL Injection), CVSS 9.3. Unauthenticated attacker can read any table the database user can access.
→ Fix: add for field in fields: self.check_alias(field) to set_values() in django/db/models/sql/query.py. Commit: django/django@f4af67b9

Four reasons this vulnerability hides from line-by-line review

  • The fix is 2 lines. In context, a 2-line addition to a method looks trivial — easily dismissed as defensive hardening rather than a critical patch. Human reviewers treat small diffs as low-priority.
  • Framework code gets implicit trust. Django's ORM internals are battle-tested and rarely questioned. The vulnerability lives in a non-obvious path — how JSONField data flows to SQL aliases — not in user-provided input visible at the call site.
  • No obvious "user input" in the diff. The vulnerability isn't in the JSONField definition or the .values() call — it was a missing check_alias() call inside set_values(). The attack surface is invisible unless you model the full data flow.
  • check_alias() existing makes the fix look redundant. Seeing the validator already exists in the same file can mislead reviewers into thinking "it's already protected" — when the real bug was that it wasn't called on the values() path.

What makes this a PullLight-specialized catch

  • Cross-method reference: PullLight models all methods in a file simultaneously. It would flag that check_alias() exists but is not called in set_values() — while it is called in add_annotation(). That gap is a specialized pattern.
  • Semantic type: CWE-89. SQL injection is a recognized vulnerability class that requires tracking how data becomes part of a SQL query. PullLight models the ORM's query construction pipeline, not just syntax.
  • JSONField path lookups as alias generation: Recognizing that data__key in values() calls is semantically dangerous alias generation — not just a lookup syntax — requires understanding how Django's ORM translates key paths to SQL column aliases.
  • "Method exists but is not called on these inputs": This is a PullLight specialty. The existing validator makes the missing call look like a minor oversight, but the oversight has CVSS 9.3 consequences.

Aug 6, 2024 Django releases 5.0.8 and 4.2.15 security updates. CVE-2024-42005 disclosed.
Aug 6, 2024 Reported by Eyal Gabay (EyalSec) via HackerOne (report #2646493)
Aug 6, 2024 Fix committed to main, 5.1, 5.0, and 4.2 branches simultaneously
Aug 6, 2024 NVD publishes CVE-2024-42005 with CVSS 9.3 base score
Jun 2026 PullLight documents CVE-2024-42005 as 17th CVE case study — demonstrating that "method exists but not called on these inputs" is a PullLight specialty catch

Don't let this happen to your PRs

More CVE Case Studies