Input validation / Query construction

MEDIUM
django/django
Commit: 3b161e60964a
Affected: Django 5.1.x pre-fix (any 5.1.x release before commit 3b161e60964aff99eddcd2627a486d81c1836b3a)
2026-04-05 14:33 UTC

Description

The commit adds a guard in When.__init__ to prevent internal keyword arguments (_connector, _negated) from propagating to Q(**lookups). If such invalid kwargs are present in the lookups passed to When, it raises a TypeError with a descriptive message listing the invalid kwargs. This is defensive input-validation hardening in query construction to prevent misuse of internal flags during query building. The change appears to be a genuine fix focused on validating inputs rather than a general code cleanup or a test-only change.

Proof of Concept

Python PoC (requires Django environment): from django.db.models.expressions import When try: # Before this fix, propagating internal kwargs to Q() could lead to # unclear errors or misuse of internal flags. The fix rejects these # and raises a clear TypeError. When(_negated=True, _connector="evil") except TypeError as e: print(str(e)) # Expected output (as per the fix): # The following kwargs are invalid: '_connector', '_negated'

Commit Details

Author: varunkasyap

Date: 2026-04-02 09:26 UTC

Message:

Fixed #37016 -- Avoided propagating invalid arguments from When() to Q().

Triage Assessment

Vulnerability Type: Input validation / Query construction

Confidence: MEDIUM

Reasoning:

The commit adds a guard to prevent certain invalid keyword arguments (_connector, _negated) from propagating from When() to Q(), raising a TypeError with a descriptive message. This is a defensive input-validation hardening in query construction, aiming to prevent unintended or unsafe query construction paths. While not a classic external vulnerability like XSS/SQLi, it tightens security-relevant behavior in how queries are built, addressing potential misuse of internal kwargs.

Verification Assessment

Vulnerability Type: Input validation / Query construction

Confidence: MEDIUM

Affected Versions: Django 5.1.x pre-fix (any 5.1.x release before commit 3b161e60964aff99eddcd2627a486d81c1836b3a)

Code Diff

diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 0c58e7749c24..6c11830d9c49 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -12,7 +12,7 @@ from django.db import DatabaseError, NotSupportedError, connection from django.db.models import fields from django.db.models.constants import LOOKUP_SEP -from django.db.models.query_utils import Q +from django.db.models.query_utils import PROHIBITED_FILTER_KWARGS, Q from django.utils.deconstruct import deconstructible from django.utils.functional import cached_property, classproperty from django.utils.hashable import make_hashable @@ -1640,6 +1640,9 @@ class When(Expression): def __init__(self, condition=None, then=None, **lookups): if lookups: + if invalid_kwargs := PROHIBITED_FILTER_KWARGS.intersection(lookups): + invalid_str = ", ".join(f"'{k}'" for k in sorted(invalid_kwargs)) + raise TypeError(f"The following kwargs are invalid: {invalid_str}") if condition is None: condition, lookups = Q(**lookups), None elif getattr(condition, "conditional", False): diff --git a/django/db/models/query.py b/django/db/models/query.py index dca504e44179..43b55a76d0cc 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -29,7 +29,7 @@ from django.db.models.expressions import Case, DatabaseDefault, F, OrderBy, Value, When from django.db.models.fetch_modes import FETCH_ONE from django.db.models.functions import Cast, Trunc -from django.db.models.query_utils import FilteredRelation, Q +from django.db.models.query_utils import PROHIBITED_FILTER_KWARGS, FilteredRelation, Q from django.db.models.sql.constants import GET_ITERATOR_CHUNK_SIZE, ROW_COUNT from django.db.models.utils import ( AltersData, @@ -46,8 +46,6 @@ # The maximum number of items to display in a QuerySet.__repr__ REPR_OUTPUT_SIZE = 20 -PROHIBITED_FILTER_KWARGS = frozenset(["_connector", "_negated"]) - class BaseIterable: def __init__( diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index 8920977cd270..a17274fba0dc 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -21,6 +21,8 @@ logger = logging.getLogger("django.db.models") +PROHIBITED_FILTER_KWARGS = frozenset(["_connector", "_negated"]) + # PathInfo is used when converting lookups (fk__somecol). The contents # describe the relation in Model terms (model Options and Fields for both # sides of the relation. The join_field is the field backing the relation. diff --git a/tests/expressions_case/tests.py b/tests/expressions_case/tests.py index 8704a7b9919f..1fde24f14924 100644 --- a/tests/expressions_case/tests.py +++ b/tests/expressions_case/tests.py @@ -1668,6 +1668,11 @@ def test_invalid_when_constructor_args(self): with self.assertRaisesMessage(TypeError, msg): When() + def test_when_rejects_invalid_arguments(self): + msg = "The following kwargs are invalid: '_connector', '_negated'" + with self.assertRaisesMessage(TypeError, msg): + When(_negated=True, _connector="evil") + def test_empty_q_object(self): msg = "An empty Q() can't be used as a When() condition." with self.assertRaisesMessage(ValueError, msg):
← Back to Alerts View on GitHub →