summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacob Walls <jacobtylerwalls@gmail.com>2026-01-19 15:42:33 -0500
committerJacob Walls <jacobtylerwalls@gmail.com>2026-02-03 08:25:13 -0500
commita14363102d98fa29b8cced578eb3a0fadaa5bcb7 (patch)
tree2bdc10fc99f861027d3dcc8cd483d0030a2571c3
parentf578acc8c54530fffabd52d2db654c8669b011af (diff)
[4.2.x] Fixed CVE-2026-1207 -- Prevented SQL injections in RasterField lookups via band index.
Thanks Tarek Nakkouch for the report, and Simon Charette for the initial triage and review. Backport of 81aa5292967cd09319c45fe2c1a525ce7b6684d8 from main.
-rw-r--r--django/contrib/gis/db/backends/postgis/operations.py6
-rw-r--r--docs/releases/4.2.28.txt12
-rw-r--r--tests/gis_tests/rasterapp/test_rasterfield.py47
3 files changed, 64 insertions, 1 deletions
diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py
index b68db377f8..d18ddab525 100644
--- a/django/contrib/gis/db/backends/postgis/operations.py
+++ b/django/contrib/gis/db/backends/postgis/operations.py
@@ -51,6 +51,9 @@ class PostGISOperator(SpatialOperator):
# Look for band indices and inject them if provided.
if lookup.band_lhs is not None and lhs_is_raster:
+ if not isinstance(lookup.band_lhs, int):
+ name = lookup.band_lhs.__class__.__name__
+ raise TypeError(f"Band index must be an integer, but got {name!r}.")
if not self.func:
raise ValueError(
"Band indices are not allowed for this operator, it works on bbox "
@@ -62,6 +65,9 @@ class PostGISOperator(SpatialOperator):
)
if lookup.band_rhs is not None and rhs_is_raster:
+ if not isinstance(lookup.band_rhs, int):
+ name = lookup.band_rhs.__class__.__name__
+ raise TypeError(f"Band index must be an integer, but got {name!r}.")
if not self.func:
raise ValueError(
"Band indices are not allowed for this operator, it works on bbox "
diff --git a/docs/releases/4.2.28.txt b/docs/releases/4.2.28.txt
index 67d398308c..aa06882806 100644
--- a/docs/releases/4.2.28.txt
+++ b/docs/releases/4.2.28.txt
@@ -29,3 +29,15 @@ produced super-linear computation resulting in service degradation or outage.
This issue has severity "moderate" according to the :ref:`Django security
policy <security-disclosure>`.
+
+CVE-2026-1207: Potential SQL injection via raster lookups on PostGIS
+====================================================================
+
+:ref:`Raster lookups <spatial-lookup-raster>` on GIS fields (only implemented
+on PostGIS) were subject to SQL injection if untrusted data was used as a band
+index.
+
+As a reminder, all untrusted user input should be validated before use.
+
+This issue has severity "high" according to the :ref:`Django security policy
+<security-disclosure>`.
diff --git a/tests/gis_tests/rasterapp/test_rasterfield.py b/tests/gis_tests/rasterapp/test_rasterfield.py
index 3f2ce770a9..89c4ec4856 100644
--- a/tests/gis_tests/rasterapp/test_rasterfield.py
+++ b/tests/gis_tests/rasterapp/test_rasterfield.py
@@ -2,7 +2,11 @@ import json
from django.contrib.gis.db.models.fields import BaseSpatialField
from django.contrib.gis.db.models.functions import Distance
-from django.contrib.gis.db.models.lookups import DistanceLookupBase, GISLookup
+from django.contrib.gis.db.models.lookups import (
+ DistanceLookupBase,
+ GISLookup,
+ RasterBandTransform,
+)
from django.contrib.gis.gdal import GDALRaster
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.measure import D
@@ -356,6 +360,47 @@ class RasterFieldTest(TransactionTestCase):
with self.assertRaisesMessage(ValueError, msg):
qs.count()
+ def test_lookup_invalid_band_rhs(self):
+ rast = GDALRaster(json.loads(JSON_RASTER))
+ qs = RasterModel.objects.filter(rast__contains=(rast, "evil"))
+ msg = "Band index must be an integer, but got 'str'."
+ with self.assertRaisesMessage(TypeError, msg):
+ qs.count()
+
+ def test_lookup_invalid_band_lhs(self):
+ """
+ Typical left-hand side usage is protected against non-integers, but for
+ defense-in-depth purposes, construct custom lookups that evade the
+ `int()` and `+ 1` checks in the lookups shipped by django.contrib.gis.
+ """
+
+ # Evade the int() call in RasterField.get_transform().
+ class MyRasterBandTransform(RasterBandTransform):
+ band_index = "evil"
+
+ def process_band_indices(self, *args, **kwargs):
+ self.band_lhs = self.lhs.band_index
+ self.band_rhs, *self.rhs_params = self.rhs_params
+
+ # Evade the `+ 1` call in BaseSpatialField.process_band_indices().
+ ContainsLookup = RasterModel._meta.get_field("rast").get_lookup("contains")
+
+ class MyContainsLookup(ContainsLookup):
+ def process_band_indices(self, *args, **kwargs):
+ self.band_lhs = self.lhs.band_index
+ self.band_rhs, *self.rhs_params = self.rhs_params
+
+ RasterField = RasterModel._meta.get_field("rast")
+ RasterField.register_lookup(MyContainsLookup, "contains")
+ self.addCleanup(RasterField.register_lookup, ContainsLookup, "contains")
+
+ qs = RasterModel.objects.annotate(
+ transformed=MyRasterBandTransform("rast")
+ ).filter(transformed__contains=(F("transformed"), 1))
+ msg = "Band index must be an integer, but got 'str'."
+ with self.assertRaisesMessage(TypeError, msg):
+ list(qs)
+
def test_isvalid_lookup_with_raster_error(self):
qs = RasterModel.objects.filter(rast__isvalid=True)
msg = (