Skip to content

Commit 6943d61

Browse files
charettessarahboyce
authored andcommitted
[5.1.x] Fixed CVE-2024-53908 -- Prevented SQL injections in direct HasKeyLookup usage on Oracle.
Thanks Seokchan Yoon for the report, and Mariusz Felisiak and Sarah Boyce for the reviews.
1 parent bbc74a7 commit 6943d61

File tree

5 files changed

+71
-18
lines changed

5 files changed

+71
-18
lines changed

django/db/models/fields/json.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -193,20 +193,18 @@ def compile_json_path_final_key(self, key_transform):
193193
# Compile the final key without interpreting ints as array elements.
194194
return ".%s" % json.dumps(key_transform)
195195

196-
def as_sql(self, compiler, connection, template=None):
196+
def _as_sql_parts(self, compiler, connection):
197197
# Process JSON path from the left-hand side.
198198
if isinstance(self.lhs, KeyTransform):
199-
lhs, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs(
199+
lhs_sql, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs(
200200
compiler, connection
201201
)
202202
lhs_json_path = compile_json_path(lhs_key_transforms)
203203
else:
204-
lhs, lhs_params = self.process_lhs(compiler, connection)
204+
lhs_sql, lhs_params = self.process_lhs(compiler, connection)
205205
lhs_json_path = "$"
206-
sql = template % lhs
207206
# Process JSON path from the right-hand side.
208207
rhs = self.rhs
209-
rhs_params = []
210208
if not isinstance(rhs, (list, tuple)):
211209
rhs = [rhs]
212210
for key in rhs:
@@ -217,24 +215,43 @@ def as_sql(self, compiler, connection, template=None):
217215
*rhs_key_transforms, final_key = rhs_key_transforms
218216
rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False)
219217
rhs_json_path += self.compile_json_path_final_key(final_key)
220-
rhs_params.append(lhs_json_path + rhs_json_path)
218+
yield lhs_sql, lhs_params, lhs_json_path + rhs_json_path
219+
220+
def _combine_sql_parts(self, parts):
221221
# Add condition for each key.
222222
if self.logical_operator:
223-
sql = "(%s)" % self.logical_operator.join([sql] * len(rhs_params))
224-
return sql, tuple(lhs_params) + tuple(rhs_params)
223+
return "(%s)" % self.logical_operator.join(parts)
224+
return "".join(parts)
225+
226+
def as_sql(self, compiler, connection, template=None):
227+
sql_parts = []
228+
params = []
229+
for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts(
230+
compiler, connection
231+
):
232+
sql_parts.append(template % (lhs_sql, "%s"))
233+
params.extend(lhs_params + [rhs_json_path])
234+
return self._combine_sql_parts(sql_parts), tuple(params)
225235

226236
def as_mysql(self, compiler, connection):
227237
return self.as_sql(
228-
compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %%s)"
238+
compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %s)"
229239
)
230240

231241
def as_oracle(self, compiler, connection):
232-
sql, params = self.as_sql(
233-
compiler, connection, template="JSON_EXISTS(%s, '%%s')"
234-
)
235-
# Add paths directly into SQL because path expressions cannot be passed
236-
# as bind variables on Oracle.
237-
return sql % tuple(params), []
242+
template = "JSON_EXISTS(%s, '%s')"
243+
sql_parts = []
244+
params = []
245+
for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts(
246+
compiler, connection
247+
):
248+
# Add right-hand-side directly into SQL because it cannot be passed
249+
# as bind variables to JSON_EXISTS. It might result in invalid
250+
# queries but it is assumed that it cannot be evaded because the
251+
# path is JSON serialized.
252+
sql_parts.append(template % (lhs_sql, rhs_json_path))
253+
params.extend(lhs_params)
254+
return self._combine_sql_parts(sql_parts), tuple(params)
238255

239256
def as_postgresql(self, compiler, connection):
240257
if isinstance(self.rhs, KeyTransform):
@@ -246,7 +263,7 @@ def as_postgresql(self, compiler, connection):
246263

247264
def as_sqlite(self, compiler, connection):
248265
return self.as_sql(
249-
compiler, connection, template="JSON_TYPE(%s, %%s) IS NOT NULL"
266+
compiler, connection, template="JSON_TYPE(%s, %s) IS NOT NULL"
250267
)
251268

252269

@@ -455,9 +472,9 @@ def as_oracle(self, compiler, connection):
455472
return "(NOT %s OR %s IS NULL)" % (sql, lhs), tuple(params) + tuple(lhs_params)
456473

457474
def as_sqlite(self, compiler, connection):
458-
template = "JSON_TYPE(%s, %%s) IS NULL"
475+
template = "JSON_TYPE(%s, %s) IS NULL"
459476
if not self.rhs:
460-
template = "JSON_TYPE(%s, %%s) IS NOT NULL"
477+
template = "JSON_TYPE(%s, %s) IS NOT NULL"
461478
return HasKeyOrArrayIndex(self.lhs.lhs, self.lhs.key_name).as_sql(
462479
compiler,
463480
connection,

docs/releases/4.2.17.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,12 @@ Remember that absolutely NO guarantee is provided about the results of
2222
``strip_tags()`` being HTML safe. So NEVER mark safe the result of a
2323
``strip_tags()`` call without escaping it first, for example with
2424
:func:`django.utils.html.escape`.
25+
26+
CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle
27+
==========================================================================
28+
29+
Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle
30+
was subject to SQL injection if untrusted data was used as a ``lhs`` value.
31+
32+
Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through
33+
the ``__`` syntax are unaffected.

docs/releases/5.0.10.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,12 @@ Remember that absolutely NO guarantee is provided about the results of
2222
``strip_tags()`` being HTML safe. So NEVER mark safe the result of a
2323
``strip_tags()`` call without escaping it first, for example with
2424
:func:`django.utils.html.escape`.
25+
26+
CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle
27+
==========================================================================
28+
29+
Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle
30+
was subject to SQL injection if untrusted data was used as a ``lhs`` value.
31+
32+
Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through
33+
the ``__`` syntax are unaffected.

docs/releases/5.1.4.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ Remember that absolutely NO guarantee is provided about the results of
2323
``strip_tags()`` call without escaping it first, for example with
2424
:func:`django.utils.html.escape`.
2525

26+
CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle
27+
==========================================================================
28+
29+
Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle
30+
was subject to SQL injection if untrusted data was used as a ``lhs`` value.
31+
32+
Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through
33+
the ``__`` syntax are unaffected.
34+
2635
Bugfixes
2736
========
2837

tests/model_fields/test_jsonfield.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from django.db.models.expressions import RawSQL
3030
from django.db.models.fields.json import (
3131
KT,
32+
HasKey,
3233
KeyTextTransform,
3334
KeyTransform,
3435
KeyTransformFactory,
@@ -582,6 +583,14 @@ def test_has_key_deep(self):
582583
[expected],
583584
)
584585

586+
def test_has_key_literal_lookup(self):
587+
self.assertSequenceEqual(
588+
NullableJSONModel.objects.filter(
589+
HasKey(Value({"foo": "bar"}, JSONField()), "foo")
590+
).order_by("id"),
591+
self.objs,
592+
)
593+
585594
def test_has_key_list(self):
586595
obj = NullableJSONModel.objects.create(value=[{"a": 1}, {"b": "x"}])
587596
tests = [

0 commit comments

Comments
 (0)