[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 81aa529296 from main.
This commit is contained in:
Jacob Walls
2026-01-19 15:42:33 -05:00
parent f578acc8c5
commit a14363102d
3 changed files with 64 additions and 1 deletions

View File

@@ -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 "

View File

@@ -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>`.

View File

@@ -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 = (