The Django ORM's greatest strength — letting developers write database queries in pure Python — is also its most subtle attack surface. In November 2025, researchers at the University of Warwick discovered CVE-2025-64459 (CVSS 9.1): a SQL injection flaw triggered simply by passing a user-controlled dictionary into QuerySet.filter(). No raw SQL. No extra(). Just **kwargs expansion. Three months later, in February 2026, the Django Security Team patched six more vulnerabilities in a single release — including a second SQL injection (CVE-2026-1312) via order_by() — and published a candid blog post admitting the same underlying patterns keep resurfacing. With 42,880+ companies running Django in production and Django 4.2 LTS hitting end-of-life in April 2026, this is the moment to understand exactly how these injections work and how to block them.
La grande force du Django ORM — écrire des requêtes de base de données en Python pur — est aussi sa surface d’attaque la plus subtile. En novembre 2025, des chercheurs de l’Université de Warwick ont découvert CVE-2025-64459 (CVSS 9.1) : une faille d’injection SQL déclenchée simplement en passant un dictionnaire contrôlé par l’utilisateur dans QuerySet.filter(). Pas de SQL brut. Pas de extra(). Juste une expansion **kwargs. Trois mois plus tard, en février 2026, l’équipe de sécurité Django a corrigé six nouvelles vulnérabilités en une seule release — dont une deuxième injection SQL (CVE-2026-1312) via order_by() — et a publié un billet de blog reconnaissant que les mêmes patterns résurgissent sans cesse. Avec 42 880+ entreprises utilisant Django en production et la fin de vie de Django 4.2 LTS en avril 2026, c’est le moment de comprendre exactement comment ces injections fonctionnent et comment les bloquer.
filter()filter()Why Django's ORM Is Not Automatically Safe from SQL Injection
Pourquoi le Django ORM n’est pas automatiquement à l’abri des injections SQL
Django's ORM parameterizes most queries automatically, which is why many developers assume they are immune to SQL injection. That assumption is partially correct: direct field lookups like User.objects.filter(username=request.GET["u"]) are safe because Django binds the value as a parameter. The injection surface lies not in the values but in the keys — field names, lookup types, ordering aliases, and internal arguments like _connector. When user-supplied data influences what goes into the left side of a keyword argument rather than the right side, the ORM's parameterization no longer protects you.
Le Django ORM paramétrise la plupart des requêtes automatiquement, ce qui fait que de nombreux développeurs se croient à l’abri des injections SQL. Cette hypothèse est partiellement correcte : les lookups directs comme User.objects.filter(username=request.GET["u"]) sont sûrs car Django lie la valeur comme paramètre. La surface d’injection se trouve non pas dans les valeurs mais dans les clés — noms de champs, types de lookup, alias d’ordre, et arguments internes comme _connector. Quand des données utilisateur influencent ce qui va dans le côté gauche d’un argument mot-clé plutôt que le côté droit, la paramétrisation de l’ORM ne vous protège plus.
The Django Security Team itself acknowledged this recurring pattern in a February 2026 blog post: "Almost every report we receive now is a variation on a prior vulnerability, exploring how an underlying pattern from a recent advisory might surface in a similar code path or under a slightly different configuration." The implication for developers: there will be more variants. The defense must go beyond patching — it requires understanding the pattern.
L’équipe de sécurité Django elle-même a reconnu ce pattern récurrent dans un billet de blog en février 2026 : « Presque chaque rapport que nous recevons est maintenant une variation d’une vulnérabilité antérieure, explorant comment un pattern sous-jacent issu d’un avis récent pourrait réapparaître dans un chemin de code similaire ou sous une configuration légèrement différente. » L’implication pour les développeurs : il y aura d’autres variantes. La défense doit aller au-delà du patching — elle exige de comprendre le pattern.
CVE-2025-64459 (CVSS 9.1): SQL Injection via the _connector Keyword
CVE-2025-64459 (CVSS 9.1) : Injection SQL via le mot-clé _connector
Discovered by Cyberstan researchers at the University of Warwick, CVE-2025-64459 affects QuerySet.filter(), QuerySet.exclude(), QuerySet.get(), and the Q() class. The vulnerability exists because Django exposes an internal argument, _connector, which controls how subclauses are joined (AND vs OR). When a user-controlled dictionary is unpacked with ** into one of these methods, the _connector key bypasses parameterization and gets interpolated directly into the SQL query.
Découverte par des chercheurs de Cyberstan à l’Université de Warwick, CVE-2025-64459 affecte QuerySet.filter(), QuerySet.exclude(), QuerySet.get(), et la classe Q(). La vulnérabilité existe car Django expose un argument interne, _connector, qui contrôle comment les sous-clauses sont jointes (AND vs OR). Quand un dictionnaire contrôlé par l’utilisateur est dépliable avec ** dans une de ces méthodes, la clé _connector contourne la paramétrisation et est interpolée directement dans la requête SQL.
# User sends: {"username": "alice", "_connector": "OR 1=1 --"}
# Application naively unpacks query params into filter()
user_params = request.GET.dict()
users = User.objects.filter(**user_params)
# Generated SQL becomes:
# SELECT ... WHERE username = 'alice' OR 1=1 -- → returns ALL users
# Only allow known, safe filter keys
ALLOWED_FILTER_FIELDS = {"username", "email", "is_active"}
def safe_filter(queryset, user_params):
safe_params = {
k: v for k, v in user_params.items()
if k in ALLOWED_FILTER_FIELDS
}
return queryset.filter(**safe_params)
The CVSS vector AV:N/AC:L/PR:N/UI:N/C:H/I:H/A:N means no authentication is required and complexity is low. Any endpoint that passes request parameters directly into ORM methods via dictionary unpacking is exploitable by an unauthenticated attacker. Successful exploitation can lead to authentication bypass, privilege escalation, or full database read.
Le vecteur CVSS AV:N/AC:L/PR:N/UI:N/C:H/I:H/A:N signifie qu’aucune authentification n’est requise et que la complexité est faible. Tout endpoint qui passe des paramètres de requête directement dans des méthodes ORM via l’expansion de dictionnaire est exploitable par un attaquant non authentifié. Une exploitation réussie peut mener à un contournement d’authentification, une escalade de privilèges, ou une lecture complète de la base de données.
Fix: Django now validates the
_connector argument against a strict allowlist of valid connector values before building the SQL clause.PoC available: Yes — public GitHub repository (stanly363/CVE-2025-64459-Poc, University of Warwick)
CVE-2026-1312: SQL Injection via order_by() and FilteredRelation Aliases
CVE-2026-1312 : Injection SQL via order_by() et les alias FilteredRelation
Patched in Django's February 3, 2026 security release alongside five other vulnerabilities, CVE-2026-1312 exploits the intersection of two ORM features: FilteredRelation (which lets you annotate queries with conditional joins) and order_by(). The bug: when an alias defined in FilteredRelation contains a period character, order_by() incorrectly interprets it as a raw table.column reference — a code path that skips parameterization and passes the alias through to SQL as-is.
Corrigée dans la release de sécurité Django du 3 février 2026 à côté de cinq autres vulnérabilités, CVE-2026-1312 exploite l’intersection de deux fonctionnalités ORM : FilteredRelation (qui permet d’annoter les requêtes avec des jointures conditionnelles) et order_by(). Le bug : quand un alias défini dans FilteredRelation contient un point, order_by() l’interprète incorrectement comme une référence brute table.colonne — un chemin de code qui saute la paramétrisation et passe l’alias tel quel dans le SQL.
from django.db.models import FilteredRelation, Q
# Alias contains a period: "recent.orders"
# If alias is user-controlled or derived from user input:
alias_name = request.GET.get("alias", "orders")
qs = Order.objects.annotate(
**{alias_name: FilteredRelation("items", condition=Q(items__active=True))}
).order_by(alias_name)
# If alias_name = "injected.alias--", order_by() passes it raw → SQL injection
Unlike CVE-2025-64459, this vulnerability requires the attacker to control the alias name — either directly (if your code exposes alias selection to users) or indirectly (via chained APIs). The impact is still serious: ORDER BY injection enables time-based and error-based data exfiltration even without direct data modification. Fixed in Django 6.0.2, 5.2.11, and 4.2.28.
Contrairement à CVE-2025-64459, cette vulnérabilité nécessite que l’attaquant contrôle le nom de l’alias — soit directement (si votre code expose la sélection d’alias aux utilisateurs) soit indirectement (via des API chaînées). L’impact reste sérieux : l’injection dans ORDER BY permet l’exfiltration de données par canal auxiliaire temporel ou basé sur les erreurs, même sans modification directe des données. Corrigé dans Django 6.0.2, 5.2.11, et 4.2.28.
All Django CVEs Addressed in 2026 (February & April Releases)
Toutes les CVE Django traitées en 2026 (releases de février & avril)
| CVE | Severity | Type | Vector | Fixed in |
|---|---|---|---|---|
| CVE | Gravité | Type | Vecteur | Corrigé dans |
CVE-2025-64459 |
CVSS 9.1 | SQL injection via _connector kwarg |
Injection SQL via _connector kwarg |
Django 5.1.14, 4.2.26, 5.2.8 |
CVE-2026-1312 |
HIGH | SQL injection via order_by() + FilteredRelation |
Injection SQL via order_by() + FilteredRelation |
Django 6.0.2, 5.2.11, 4.2.28 |
CVE-2026-1207 |
HIGH | DoS via malformed HTTP multipart data | DoS via données HTTP multipart malformées | Django 6.0.2, 5.2.11, 4.2.28 |
CVE-2026-1287 |
HIGH | User enumeration via timing attack (login/password reset) | Enumération d’utilisateurs via attaque temporelle | Django 6.0.2, 5.2.11, 4.2.28 |
CVE-2026-1288 |
MEDIUM | DoS via pathological regex in EmailValidator |
DoS via regex pathologique dans EmailValidator |
Django 6.0.2, 5.2.11, 4.2.28 |
CVE-2026-1289 |
MEDIUM | DoS via unbounded URL redirect validation | DoS via validation de redirection URL non bornée | Django 6.0.2, 5.2.11, 4.2.28 |
In April 2026, the Django team issued a further security release (Django 6.0.4, 5.2.13, 4.2.30) addressing additional issues discovered post-February. If you have not upgraded since February, you need two upgrades. Simultaneously, Django 4.2 LTS reached end-of-life in April 2026 — meaning no further security patches will be issued for it. Teams still running 4.2.x should treat migration to 5.2 LTS (supported until April 2028) as an urgent task.
En avril 2026, l’équipe Django a publié une nouvelle release de sécurité (Django 6.0.4, 5.2.13, 4.2.30) corrigeant des problèmes supplémentaires découverts après février. Si vous n’avez pas mis à jour depuis février, vous avez besoin de deux mises à jour. Parallèlement, Django 4.2 LTS a atteint sa fin de vie en avril 2026 — ce qui signifie qu’aucun correctif de sécurité supplémentaire ne sera émis. Les équipes qui utilisent encore la 4.2.x doivent traiter la migration vers 5.2 LTS (supporté jusqu’à avril 2028) comme une tâche urgente.
The Underlying Pattern: Why Django ORM Injection Keeps Coming Back
Le Pattern Sous-Jacent : Pourquoi les Injections ORM Django Reviennent Sans Cesse
The Django Security Team's February 2026 blog post identified three vulnerability categories that collectively explain the recurring injections: SQL injection through unsanitized input passed to ORM features, denial-of-service through malformed input handling, and user enumeration via timing attacks. All three are framework-level patterns, not isolated bugs. The root cause for the SQL injections specifically: Django's ORM exposes internal symbolic arguments (_connector, _negated) that are designed to be used by trusted code — but the API surface does not prevent user-controlled data from reaching them when developers use dynamic kwargs expansion.
Le billet de blog de l’équipe de sécurité Django de février 2026 a identifié trois catégories de vulnérabilités qui expliquent collectivement les injections récurrentes : injection SQL via des entrées non nettoyées passées aux fonctionnalités ORM, déni de service via des entrées malformées, et enumération d’utilisateurs via des attaques temporelles. Les trois sont des patterns au niveau du framework, pas des bugs isolés. La cause profonde des injections SQL : l’ORM Django expose des arguments symboliques internes (_connector, _negated) conçus pour être utilisés par du code de confiance — mais la surface API n’empêche pas les données contrôlées par l’utilisateur de les atteindre quand les développeurs utilisent l’expansion dynamique de kwargs.
Historical data confirms this is a systemic issue: in 2024, Django had 13 security vulnerabilities published. In 2025, 6 vulnerabilities with an average CVSS score of 6.9. The pattern of SQL injection via ORM internals goes back to CVE-2022-34265 (Trunc() and Extract() injection) and CVE-2024-42005 (QuerySet.values() injection). Each time, the fix is surgical — blocking one specific code path — while adjacent code paths remain vulnerable until discovered.
Les données historiques confirment qu’il s’agit d’un problème systémique : en 2024, Django a eu 13 vulnérabilités de sécurité publiées. En 2025, 6 vulnérabilités avec un score CVSS moyen de 6,9. Le pattern d’injection SQL via les internals de l’ORM remonte à CVE-2022-34265 (injection via Trunc() et Extract()) et CVE-2024-42005 (injection via QuerySet.values()). À chaque fois, le correctif est chirurgical — bloquant un chemin de code spécifique — tandis que les chemins de code adjacents restent vulnérables jusqu’à ce qu’ils soient découverts.
Django ORM Security Hardening Guide for 2026
Guide de Durcissement de la Sécurité ORM Django pour 2026
The following practices protect against the current CVEs and the variants the Django Security Team warns will follow.
Les pratiques suivantes protègent contre les CVE actuelles et les variantes que l’équipe de sécurité Django avertit suivront.
1. Never pass user-controlled dicts directly to ORM methods
1. Ne jamais passer des dictionnaires contrôlés par l’utilisateur directement aux méthodes ORM
# NEVER do this:
User.objects.filter(**request.GET.dict())
User.objects.filter(**request.POST.dict())
# ALWAYS do this — explicit allowlist:
ALLOWED_FIELDS = {"username", "email", "role", "is_active"}
def filter_from_request(qs, params):
safe = {k: v for k, v in params.items() if k in ALLOWED_FIELDS}
return qs.filter(**safe)
2. Never expose dynamic order_by values from user input
2. Ne jamais exposer des valeurs order_by dynamiques depuis l’entrée utilisateur
# NEVER do this:
field = request.GET.get("sort", "id")
qs.order_by(field) # field could be "injected.alias--"
# NEVER do this with FilteredRelation aliases either:
alias = request.GET.get("alias", "default")
qs.annotate(**{alias: FilteredRelation(...)}).order_by(alias)
# SAFE: validate against a strict enum of allowed sort fields
ALLOWED_SORTS = {
"name": "name",
"date": "-created_at",
"price": "price",
}
sort_param = request.GET.get("sort", "name")
order_field = ALLOWED_SORTS.get(sort_param, "name")
qs.order_by(order_field)
3. Run on a patched version — Django 5.2.13+ or 6.0.4+
3. Utiliser une version corrigée — Django 5.2.13+ ou 6.0.4+
• Django 5.2 LTS (5.2.13+) — supported until April 2028 ✅
• Django 6.0 (6.0.4+) — supported ✅
• Django 4.2 LTS — END OF LIFE April 2026 ❌ — no more security patches
• Django 5.0/5.1 — unsupported, not evaluated for CVE-2026-1312 ❌ • Django 5.2 LTS (5.2.13+) — supporté jusqu’en avril 2028 ✅
• Django 6.0 (6.0.4+) — supporté ✅
• Django 4.2 LTS — FIN DE VIE avril 2026 ❌ — plus de correctifs de sécurité
• Django 5.0/5.1 — non supporté, non évalué pour CVE-2026-1312 ❌
4. Additional ORM hardening practices
4. Pratiques supplémentaires de durcissement ORM
- Prefer
.values("field1", "field2")with explicit field lists over.values()with no arguments — and never pass user-supplied strings as field names to.values()Préférer.values("field1", "field2")avec des listes de champs explicites plutôt que.values()sans arguments — et ne jamais passer des chaînes fournies par l’utilisateur comme noms de champs à.values() - Avoid using
.extra()entirely — deprecated since Django 1.x and always requires raw SQL strings. Useannotate()with ORM expressions insteadEviter complètement.extra()— déprécié depuis Django 1.x et toujours basé sur des chaînes SQL brutes. Utiliserannotate()avec des expressions ORM à la place - When using
RawSQL()orraw(), always use positional parameters (%s) — never use Python f-strings or.format()to build raw SQLQuand vous utilisezRawSQL()ouraw(), toujours utiliser des paramètres positionnels (%s) — ne jamais utiliser les f-strings Python ou.format()pour construire du SQL brut - Use Django's
CONN_MAX_AGEwith care — persistent connections amplify the damage if an injection leads to session data poisoningUtiliserCONN_MAX_AGEde Django avec précaution — les connexions persistantes amplifient les dégâts si une injection mène à un empoisonnement de données de session - Enable Django's
ATOMIC_REQUESTS = True— wraps each request in a transaction, preventing partial-write attacks in some injection scenariosActiverATOMIC_REQUESTS = Truede Django — enveloppe chaque requête dans une transaction, évitant les attaques d’écriture partielle dans certains scénarios d’injection - Run static analysis with
bandit(rules B608, B703) andsemgreprules for Django to catch ORM misuse patterns in CIExécuter une analyse statique avecbandit(règles B608, B703) et des règlessemgreppour Django afin de détecter les patterns de mauvais usage ORM dans CI - Pin your Django version in
requirements.txtorpyproject.tomland automate monitoring — the February 2026 release was 9 days old before many teams had patchedFixer votre version Django dansrequirements.txtoupyproject.tomlet automatiser le monitoring — la release de février 2026 avait 9 jours avant que de nombreuses équipes aient patché
How to Detect Vulnerable ORM Patterns in Your Codebase
Comment Détecter les Patterns ORM Vulnérables dans Votre Codebase
The most dangerous pattern is dynamic **kwargs expansion into ORM methods. You can find it quickly with a grep across your project:
Le pattern le plus dangereux est l’expansion dynamique de **kwargs dans les méthodes ORM. Vous pouvez le trouver rapidement avec un grep dans votre projet :
# Find potential dynamic filter() expansions
grep -rn "\\.filter(\*\*" . --include="*.py"
grep -rn "\\.exclude(\*\*" . --include="*.py"
grep -rn "\\.get(\*\*" . --include="*.py"
grep -rn "\\.order_by(" . --include="*.py"
# Look specifically for request data flowing into ORM calls
grep -rn "request\.GET\.dict\|request\.POST\.dict\|request\.data" . \
--include="*.py" | grep -v "^Binary"
# Semgrep rule (add to your CI pipeline)
# rules:
# - id: django-orm-dynamic-kwargs
# pattern: $QS.filter(**$USER_INPUT)
# message: "Dynamic kwargs expansion into ORM filter()"
# severity: ERROR
For the order_by() vulnerability specifically, look for any place where a string derived from user input (request params, API payloads, database values from untrusted sources) is passed to order_by() without going through a strict allowlist.
Pour la vulnérabilité order_by() spécifiquement, cherchez tout endroit où une chaîne dérivée d’une entrée utilisateur (paramètres de requête, payloads API, valeurs de base de données provenant de sources non fiables) est passée à order_by() sans passer par une liste blanche stricte.
Frequently Asked Questions
Questions fréquentes
Is Django's ORM parameterized by default?
Le Django ORM est-il paramétrisé par défaut ?
Partially. Django parameterizes query values (the right side of field lookups) automatically. However, query structure — field names, operators, aliases, and internal arguments — is not parameterized. When user-controlled data influences query structure via **kwargs expansion or dynamic argument names, SQL injection becomes possible even with the ORM.
Partiellement. Django paramétrise automatiquement les valeurs de requêtes (le côté droit des lookups de champs). Cependant, la structure des requêtes — noms de champs, opérateurs, alias, et arguments internes — n’est pas paramétrisée. Quand des données contrôlées par l’utilisateur influencent la structure de requête via l’expansion de **kwargs ou des noms d’arguments dynamiques, l’injection SQL devient possible même avec l’ORM.
Does using Django REST Framework protect against CVE-2025-64459?
L’utilisation de Django REST Framework protège-t-elle contre CVE-2025-64459 ?
Not automatically. DRF's FilterBackend classes (DjangoFilterBackend, etc.) add a layer of field validation, but if a custom filter class or viewset passes request params directly into queryset.filter(**params), the underlying Django vulnerability applies regardless of DRF. The protection must be implemented at the ORM call site via an explicit allowlist.
Pas automatiquement. Les classes FilterBackend de DRF (DjangoFilterBackend, etc.) ajoutent une couche de validation de champs, mais si une classe de filtre personnalisée ou un viewset passe les paramètres de requête directement dans queryset.filter(**params), la vulnérabilité Django sous-jacente s’applique indépendamment de DRF. La protection doit être implémentée au niveau de l’appel ORM via une liste blanche explicite.
Is Django 4.2 still safe to run after end-of-life?
Django 4.2 est-il encore sûr après la fin de vie ?
No. Django 4.2 LTS reached end-of-life in April 2026. The Django project will no longer issue security patches for 4.2.x. Any vulnerability discovered after this date — including future variants of the ORM injection pattern — will not be patched for 4.2. Teams must migrate to Django 5.2 LTS (supported until April 2028) as a priority.
Non. Django 4.2 LTS a atteint sa fin de vie en avril 2026. Le projet Django n’émettra plus de correctifs de sécurité pour 4.2.x. Toute vulnérabilité découverte après cette date — y compris les futures variantes du pattern d’injection ORM — ne sera pas corrigée pour la 4.2. Les équipes doivent migrer vers Django 5.2 LTS (supporté jusqu’en avril 2028) en priorité.
How quickly was CVE-2025-64459 exploited after disclosure?
Combien de temps après la divulgation CVE-2025-64459 a-t-elle été exploitée ?
A public proof-of-concept was available on GitHub within days of the advisory (stanly363/CVE-2025-64459-Poc). The CVSS vector indicates low attack complexity with no authentication required, meaning any attacker can run automated scans against vulnerable endpoints. Public CVE advisories with PoC code are typically weaponized within 48-72 hours of disclosure according to industry data from Security Boulevard (2026), making rapid patching critical.
Un proof-of-concept public était disponible sur GitHub dans les jours suivant l’avis (stanly363/CVE-2025-64459-Poc). Le vecteur CVSS indique une complexité d’attaque faible sans authentification requise, ce qui signifie que tout attaquant peut exécuter des scans automatisés contre les endpoints vulnérables. Les avis CVE publics avec code PoC sont typiquement transformés en armes dans les 48-72 heures selon les données industrielles (Security Boulevard, 2026), rendant le patching rapide critique.
Will there be more Django ORM injection CVEs after 2026?
Y aura-t-il d’autres CVE d’injection ORM Django après 2026 ?
The Django Security Team itself says yes — explicitly. Their February 2026 blog post states that almost every incoming security report is a variation on a prior pattern. The underlying design trade-off (exposing internal ORM arguments via public APIs) means new code paths will keep being found. The defense is a combination of patching promptly and eliminating dynamic kwargs expansion at the code level — you cannot rely on Django alone to protect you.
L’équipe de sécurité Django elle-même dit oui — explicitement. Leur billet de blog de février 2026 déclare que presque chaque rapport de sécurité entrant est une variation d’un pattern antérieur. Le compromis de conception sous-jacent (exposer les arguments ORM internes via des API publiques) signifie que de nouveaux chemins de code continueront d’être trouvés. La défense est une combinaison de patching rapide et d’élimination de l’expansion dynamique de kwargs au niveau du code — vous ne pouvez pas compter sur Django seul pour vous protéger.
Know When the Next Django CVE Drops — Before Your App Is Targeted
Sachez quand la prochaine CVE Django paraît — avant que votre app soit ciblée
CVE OptiBot scans your requirements.txt, Pipfile.lock, and poetry.lock daily against the OSV.dev database. When a new Django CVE is published — like the 6 patched in February 2026 — you get an instant alert with the affected version, CVSS score, and the exact upgrade command. No setup beyond uploading your lockfile.
CVE OptiBot scanne vos fichiers requirements.txt, Pipfile.lock et poetry.lock quotidiennement contre la base de données OSV.dev. Quand une nouvelle CVE Django est publiée — comme les 6 corrigées en février 2026 — vous recevez une alerte instantanée avec la version affectée, le score CVSS et la commande de mise à jour exacte. Aucune configuration au-delà du dépôt de votre lockfile.