mirror of
				https://github.com/django/django.git
				synced 2025-10-30 17:16:10 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
				
					committed by
					
						 Sarah Boyce
						Sarah Boyce
					
				
			
			
				
	
			
			
			
						parent
						
							49ff1042aa
						
					
				
				
					commit
					8f8dc5a1fc
				
			| @@ -193,20 +193,18 @@ class HasKeyLookup(PostgresOperatorLookup): | |||||||
|         # Compile the final key without interpreting ints as array elements. |         # Compile the final key without interpreting ints as array elements. | ||||||
|         return ".%s" % json.dumps(key_transform) |         return ".%s" % json.dumps(key_transform) | ||||||
|  |  | ||||||
|     def as_sql(self, compiler, connection, template=None): |     def _as_sql_parts(self, compiler, connection): | ||||||
|         # Process JSON path from the left-hand side. |         # Process JSON path from the left-hand side. | ||||||
|         if isinstance(self.lhs, KeyTransform): |         if isinstance(self.lhs, KeyTransform): | ||||||
|             lhs, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs( |             lhs_sql, lhs_params, lhs_key_transforms = self.lhs.preprocess_lhs( | ||||||
|                 compiler, connection |                 compiler, connection | ||||||
|             ) |             ) | ||||||
|             lhs_json_path = compile_json_path(lhs_key_transforms) |             lhs_json_path = compile_json_path(lhs_key_transforms) | ||||||
|         else: |         else: | ||||||
|             lhs, lhs_params = self.process_lhs(compiler, connection) |             lhs_sql, lhs_params = self.process_lhs(compiler, connection) | ||||||
|             lhs_json_path = "$" |             lhs_json_path = "$" | ||||||
|         sql = template % lhs |  | ||||||
|         # Process JSON path from the right-hand side. |         # Process JSON path from the right-hand side. | ||||||
|         rhs = self.rhs |         rhs = self.rhs | ||||||
|         rhs_params = [] |  | ||||||
|         if not isinstance(rhs, (list, tuple)): |         if not isinstance(rhs, (list, tuple)): | ||||||
|             rhs = [rhs] |             rhs = [rhs] | ||||||
|         for key in rhs: |         for key in rhs: | ||||||
| @@ -217,25 +215,45 @@ class HasKeyLookup(PostgresOperatorLookup): | |||||||
|             *rhs_key_transforms, final_key = rhs_key_transforms |             *rhs_key_transforms, final_key = rhs_key_transforms | ||||||
|             rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False) |             rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False) | ||||||
|             rhs_json_path += self.compile_json_path_final_key(final_key) |             rhs_json_path += self.compile_json_path_final_key(final_key) | ||||||
|             rhs_params.append(lhs_json_path + rhs_json_path) |             yield lhs_sql, lhs_params, lhs_json_path + rhs_json_path | ||||||
|  |  | ||||||
|  |     def _combine_sql_parts(self, parts): | ||||||
|         # Add condition for each key. |         # Add condition for each key. | ||||||
|         if self.logical_operator: |         if self.logical_operator: | ||||||
|             sql = "(%s)" % self.logical_operator.join([sql] * len(rhs_params)) |             return "(%s)" % self.logical_operator.join(parts) | ||||||
|         return sql, tuple(lhs_params) + tuple(rhs_params) |         return "".join(parts) | ||||||
|  |  | ||||||
|  |     def as_sql(self, compiler, connection, template=None): | ||||||
|  |         sql_parts = [] | ||||||
|  |         params = [] | ||||||
|  |         for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts( | ||||||
|  |             compiler, connection | ||||||
|  |         ): | ||||||
|  |             sql_parts.append(template % (lhs_sql, "%s")) | ||||||
|  |             params.extend(lhs_params + [rhs_json_path]) | ||||||
|  |         return self._combine_sql_parts(sql_parts), tuple(params) | ||||||
|  |  | ||||||
|     def as_mysql(self, compiler, connection): |     def as_mysql(self, compiler, connection): | ||||||
|         return self.as_sql( |         return self.as_sql( | ||||||
|             compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %%s)" |             compiler, connection, template="JSON_CONTAINS_PATH(%s, 'one', %s)" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def as_oracle(self, compiler, connection): |     def as_oracle(self, compiler, connection): | ||||||
|         sql, params = self.as_sql( |         # Use a custom delimiter to prevent the JSON path from escaping the SQL | ||||||
|             compiler, connection, template="JSON_EXISTS(%s, q'\uffff%%s\uffff')" |         # literal. See comment in KeyTransform. | ||||||
|         ) |         template = "JSON_EXISTS(%s, q'\uffff%s\uffff')" | ||||||
|         # Add paths directly into SQL because path expressions cannot be passed |         sql_parts = [] | ||||||
|         # as bind variables on Oracle. Use a custom delimiter to prevent the |         params = [] | ||||||
|         # JSON path from escaping the SQL literal. See comment in KeyTransform. |         for lhs_sql, lhs_params, rhs_json_path in self._as_sql_parts( | ||||||
|         return sql % tuple(params), [] |             compiler, connection | ||||||
|  |         ): | ||||||
|  |             # Add right-hand-side directly into SQL because it cannot be passed | ||||||
|  |             # as bind variables to JSON_EXISTS. It might result in invalid | ||||||
|  |             # queries but it is assumed that it cannot be evaded because the | ||||||
|  |             # path is JSON serialized. | ||||||
|  |             sql_parts.append(template % (lhs_sql, rhs_json_path)) | ||||||
|  |             params.extend(lhs_params) | ||||||
|  |         return self._combine_sql_parts(sql_parts), tuple(params) | ||||||
|  |  | ||||||
|     def as_postgresql(self, compiler, connection): |     def as_postgresql(self, compiler, connection): | ||||||
|         if isinstance(self.rhs, KeyTransform): |         if isinstance(self.rhs, KeyTransform): | ||||||
| @@ -247,7 +265,7 @@ class HasKeyLookup(PostgresOperatorLookup): | |||||||
|  |  | ||||||
|     def as_sqlite(self, compiler, connection): |     def as_sqlite(self, compiler, connection): | ||||||
|         return self.as_sql( |         return self.as_sql( | ||||||
|             compiler, connection, template="JSON_TYPE(%s, %%s) IS NOT NULL" |             compiler, connection, template="JSON_TYPE(%s, %s) IS NOT NULL" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -470,9 +488,9 @@ class KeyTransformIsNull(lookups.IsNull): | |||||||
|         return "(NOT %s OR %s IS NULL)" % (sql, lhs), tuple(params) + tuple(lhs_params) |         return "(NOT %s OR %s IS NULL)" % (sql, lhs), tuple(params) + tuple(lhs_params) | ||||||
|  |  | ||||||
|     def as_sqlite(self, compiler, connection): |     def as_sqlite(self, compiler, connection): | ||||||
|         template = "JSON_TYPE(%s, %%s) IS NULL" |         template = "JSON_TYPE(%s, %s) IS NULL" | ||||||
|         if not self.rhs: |         if not self.rhs: | ||||||
|             template = "JSON_TYPE(%s, %%s) IS NOT NULL" |             template = "JSON_TYPE(%s, %s) IS NOT NULL" | ||||||
|         return HasKeyOrArrayIndex(self.lhs.lhs, self.lhs.key_name).as_sql( |         return HasKeyOrArrayIndex(self.lhs.lhs, self.lhs.key_name).as_sql( | ||||||
|             compiler, |             compiler, | ||||||
|             connection, |             connection, | ||||||
|   | |||||||
| @@ -22,3 +22,12 @@ Remember that absolutely NO guarantee is provided about the results of | |||||||
| ``strip_tags()`` being HTML safe. So NEVER mark safe the result of a | ``strip_tags()`` being HTML safe. So NEVER mark safe the result of a | ||||||
| ``strip_tags()`` call without escaping it first, for example with | ``strip_tags()`` call without escaping it first, for example with | ||||||
| :func:`django.utils.html.escape`. | :func:`django.utils.html.escape`. | ||||||
|  |  | ||||||
|  | CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle | ||||||
|  | ========================================================================== | ||||||
|  |  | ||||||
|  | Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle | ||||||
|  | was subject to SQL injection if untrusted data was used as a ``lhs`` value. | ||||||
|  |  | ||||||
|  | Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through | ||||||
|  | the ``__`` syntax are unaffected. | ||||||
|   | |||||||
| @@ -22,3 +22,12 @@ Remember that absolutely NO guarantee is provided about the results of | |||||||
| ``strip_tags()`` being HTML safe. So NEVER mark safe the result of a | ``strip_tags()`` being HTML safe. So NEVER mark safe the result of a | ||||||
| ``strip_tags()`` call without escaping it first, for example with | ``strip_tags()`` call without escaping it first, for example with | ||||||
| :func:`django.utils.html.escape`. | :func:`django.utils.html.escape`. | ||||||
|  |  | ||||||
|  | CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle | ||||||
|  | ========================================================================== | ||||||
|  |  | ||||||
|  | Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle | ||||||
|  | was subject to SQL injection if untrusted data was used as a ``lhs`` value. | ||||||
|  |  | ||||||
|  | Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through | ||||||
|  | the ``__`` syntax are unaffected. | ||||||
|   | |||||||
| @@ -23,6 +23,15 @@ Remember that absolutely NO guarantee is provided about the results of | |||||||
| ``strip_tags()`` call without escaping it first, for example with | ``strip_tags()`` call without escaping it first, for example with | ||||||
| :func:`django.utils.html.escape`. | :func:`django.utils.html.escape`. | ||||||
|  |  | ||||||
|  | CVE-2024-53908: Potential SQL injection via ``HasKey(lhs, rhs)`` on Oracle | ||||||
|  | ========================================================================== | ||||||
|  |  | ||||||
|  | Direct usage of the ``django.db.models.fields.json.HasKey`` lookup on Oracle | ||||||
|  | was subject to SQL injection if untrusted data was used as a ``lhs`` value. | ||||||
|  |  | ||||||
|  | Applications that use the :lookup:`has_key <jsonfield.has_key>` lookup through | ||||||
|  | the ``__`` syntax are unaffected. | ||||||
|  |  | ||||||
| Bugfixes | Bugfixes | ||||||
| ======== | ======== | ||||||
|  |  | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ from django.db.models import ( | |||||||
| from django.db.models.expressions import RawSQL | from django.db.models.expressions import RawSQL | ||||||
| from django.db.models.fields.json import ( | from django.db.models.fields.json import ( | ||||||
|     KT, |     KT, | ||||||
|  |     HasKey, | ||||||
|     KeyTextTransform, |     KeyTextTransform, | ||||||
|     KeyTransform, |     KeyTransform, | ||||||
|     KeyTransformFactory, |     KeyTransformFactory, | ||||||
| @@ -582,6 +583,14 @@ class TestQuerying(TestCase): | |||||||
|                     [expected], |                     [expected], | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |     def test_has_key_literal_lookup(self): | ||||||
|  |         self.assertSequenceEqual( | ||||||
|  |             NullableJSONModel.objects.filter( | ||||||
|  |                 HasKey(Value({"foo": "bar"}, JSONField()), "foo") | ||||||
|  |             ).order_by("id"), | ||||||
|  |             self.objs, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_has_key_list(self): |     def test_has_key_list(self): | ||||||
|         obj = NullableJSONModel.objects.create(value=[{"a": 1}, {"b": "x"}]) |         obj = NullableJSONModel.objects.create(value=[{"a": 1}, {"b": "x"}]) | ||||||
|         tests = [ |         tests = [ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user