← 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.
TL;DR
// 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
CVSS v3.1 Vector
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
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N
Vulnerable Code — Before the Fix
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.
The Fix — After (Django 4.2.15 / 5.0.8)
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 = []
// 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().
Attack Chain — Step by Step
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.
Synthetic PR Diff — What PullLight Would Flag
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')
Why Human Review Misses This
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.
Timeline
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
Fix & Resources
Fixed in: Django 4.2.15, 5.0.8, 5.1 (rc)
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*argspassed toset_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.
for field in fields: self.check_alias(field)toset_values()indjango/db/models/sql/query.py. Commit: django/django@f4af67b9