CVE-2024-42005 and CVE-2025-57833 are two severe SQL injection vulnerabilities in Django, the most widely deployed Python web framework. Both share one root cause: a value that an application lets a client choose is used as a column alias, and column aliases are written into the SQL as identifiers, which the database driver's parameter binding cannot protect. This article walks through what each CVE is, how the injection reaches the query, why it lives in a dependency rather than in application code, and what that means for how teams watch the code they did not write.
What the two CVEs are
Both were published in Django's own security releases and are recorded in the National Vulnerability Database as CWE-89, SQL injection.
CVE-2024-42005 is a "Potential SQL injection in QuerySet.values() and values_list()", disclosed in Django's 6 August 2024 security release. Django's release notes describe it precisely: values() and values_list() on a model with a JSONField "were subject to SQL injection in column aliases, via a crafted JSON object key as a passed *arg." The Django project rated it severity "high". NVD scores CVE-2024-42005 at 7.3 (High); CISA's enrichment rates it 9.8 (Critical). It was fixed in Django 5.0.8 and 4.2.15.
CVE-2025-57833 is a "Potential SQL injection in FilteredRelation column aliases", disclosed in Django's 3 September 2025 security release. The notes read: FilteredRelation "was subject to SQL injection in column aliases, using a suitably crafted dictionary, with dictionary expansion, as the **kwargs passed to QuerySet.annotate() or QuerySet.alias()." Django rated it "high"; NVD scores it at 8.1 (High). It was fixed in Django 4.2.24, 5.1.12 and 5.2.6.
For transparency: both advisories credit Eyal Gabay of EyalSec, publisher of this site; our thanks to Django's security team for fixing both. More useful to a Django team: the bug's shape, which recurs.
How a column alias reaches the SQL
Django's ORM is, correctly, immune to classic SQL injection in the common case. When you filter on a value, the value becomes a bound parameter: the SQL text carries a placeholder, and the database driver sends the value separately. The Django security documentation states the guarantee plainly: "A query's SQL code is defined separately from the query's parameters."
A column alias is different. An alias is the name a selected expression is given in the query, the part after AS. Names are not values. A database cannot accept an identifier as a bound parameter, so Django has to write the alias into the SQL text itself, quoted as an identifier. If the alias contains a closing quote, everything after it is read by the database as more SQL.
The two CVEs are the same mechanism reached through two different ORM features:
- In CVE-2024-42005, the alias of a
JSONFieldkey lookup is derived from the key you pass tovalues(). If the application lets a client choose which JSON key to project, the client chooses an alias. - In CVE-2025-57833, the alias is a key in the
**kwargsdictionary passed toannotate()oralias()for aFilteredRelation. If the application builds that dictionary from client input, the client chooses an alias.
Here is what the application side of each looks like. Neither snippet contains raw SQL, and neither looks wrong in review:
from django.db.models import Count, FilteredRelation, Q
# CVE-2024-42005 shape: group a report by a client-chosen JSON key
def breakdown(request):
key = request.GET["group_by"]
return list(Report.objects.values(f"data__{key}").annotate(n=Count("id")))
# CVE-2025-57833 shape: named filtered relations built from a client dictionary
def books(request, filters):
rels = {
name: FilteredRelation("author", condition=Q(author__name=value))
for name, value in filters.items()
}
return Book.objects.annotate(**rels)
We reproduced both shapes against the last vulnerable and first fixed Django 4.2 releases, using SQLite and a harmless marker string rather than an attack. On Django 4.2.14, the first function wrote the client's key verbatim inside the quoted alias of the generated SELECT ... AS "data__...", while the JSON path itself stayed a bound parameter. A key containing a double quote ended the alias early. On 4.2.15 the same call raised ValueError. On Django 4.2.23, the second function placed the dictionary key, unquoted, as the alias of the joined table in the INNER JOIN clause, while value stayed a bound parameter. On 4.2.24 it raised ValueError.
The defensive takeaway is the same in both: a value that looks like "just a field name" is interpolated into SQL as an identifier, and parameterization never touches it. The value you were careful to bind was safe all along; the name next to it was not.
Why the fix is one line, and what that tells you
Django has validated aliases since CVE-2022-28346 (April 2022), which added a check_alias() method and a FORBIDDEN_ALIAS_PATTERN that rejects quotation marks, whitespace, semicolons and SQL comments in an alias. The fixes for both later CVEs, visible in the public commits linked from Django's security archive, add a single self.check_alias(...) call on a code path that reached alias construction without one:
# CVE-2024-42005 fix, in Query.set_values()
if fields:
for field in fields:
self.check_alias(field)
...
# CVE-2025-57833 fix, in Query.add_filtered_relation()
def add_filtered_relation(self, filtered_relation, alias):
self.check_alias(alias)
...
The validator existed; some call paths bypassed it, easy to miss in a large ORM. Alias injection is not one bug that got fixed; it is a class. Django has shipped several entries in the same family, including CVE-2022-28346, CVE-2025-59681, CVE-2025-13372 and CVE-2026-1287, each closing a new path into alias construction. A team that treats "we upgraded past that one CVE" as closure for the class will be surprised by the next path.
Why this lives in a dependency, not in your code
An application developer did nothing wrong at the SQL layer here. They used the ORM, which is the recommended, injection-safe interface. The flaw is in how the framework assembled the query from an API that accepted a developer-supplied name, where "developer-supplied" quietly became "client-supplied" because the application passed a request field through.
This is the uncomfortable property of a dependency vulnerability: the dangerous operation runs inside site-packages, built by code you did not write and do not review, from data your own code handed in. Static analysis of your repository sees a call to QuerySet.values(), which is not a finding. A software composition scanner can tell you that an installed Django version has a known CVE, but not whether any request on your system actually drives attacker-controlled data into the vulnerable path. The 2025 OWASP Top 10 puts both halves of this problem near the top: A05 Injection and A03 Software Supply Chain Failures, the latter ranked first in the community survey.
What your team can do
A few concrete practices follow from the shape of these bugs:
- Treat framework "name" parameters as values when a client can influence them. Any place you pass a request field as a field name, column, alias, ordering key or JSON key into the ORM is a candidate. Allowlist against a fixed set of known columns; pass only names from that set.
- Upgrade promptly, and watch the whole class. Keep Django current so a fixed release is in place, and when a new alias injection CVE lands, check your own code for the pattern it describes rather than only bumping the version.
- Get evidence from the running system, not only from a source scan. The question that matters is not "do we call
values()" but "does attacker-controlled data ever reach the query the framework builds". That is a runtime dataflow question.
What es-python showed on these two CVEs
On the last point: the EyalSec Python runtime, es-python, follows untrusted data through the program as it runs, including through framework and library code, and checks it at the operation itself, the moment the program sends a query. Because the check happens where the query is issued rather than at the network edge or in your repository, it does not matter whether the dangerous string was assembled in your module or deep inside a dependency.
We measured this directly. For each of the two CVEs we wrote a proof of concept that reconstructs the pre-fix data flow the advisory describes, marked the attacker-supplied input as untrusted, and ran it under es-python. In both cases es-python reported untrusted data reaching the database query operation, named where it entered, and, with blocking switched on, stopped the query before it ran. The same exercise covered every Django SQL injection advisory in the OSV database at the time (17 of 17 flagged). Be clear about what that number is: it measures detection of known, already-fixed bugs, not discovery of new ones, and it says nothing about the large share of Django advisories (CSRF, authorization, timing, resource exhaustion) that are not a flow of untrusted data into a dangerous operation at all.
You can read how sources, sinks and events fit together on How EyalSec works, what blocking does on Report and Raise, and the broader class on SQL injection.
Whether or not you ever run a runtime sensor, the actions are the same: upgrade Django, allowlist anything a client can use as a name, and look for the pattern in your own code each time a new alias CVE is published.
If you run a large Python backend and want to see this on your own code, you can book a live demo.