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'
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.
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):