티스토리 수익 글 보기

티스토리 수익 글 보기

[6.1.x] Fixed CVE-2026-15830 — Mitigated potential DoS via nested ge… · django/django@9e4a3f1 · GitHub
Skip to content

Commit 9e4a3f1

Browse files
jacobtylerwallsnessita
authored andcommitted
[6.1.x] Fixed CVE-2026-15830 — Mitigated potential DoS via nested geometry collections.
Since deeply nested geometry collections can lead to fatal errors in GEOS, a new `max_geom_collections` argument on geometry model and form fields, passed down to `GEOSGeometry` itself, allows limiting either depth (WKT) or total number (WKB) before reaching GEOS. Thanks Andrew MacPherson and kimchunbok_ for the reports, and Natalia Bidart, Simon Charette, and Sarah Boyce for reviews. Backport of d2e59b7 from main.
1 parent 5b3523d commit 9e4a3f1

23 files changed

Lines changed: 666 additions & 33 deletions

File tree

django/contrib/gis/db/backends/mysql/features.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,17 @@ def supports_geometry_field_unique_index(self):
2020
# Not supported in MySQL since
2121
# https://dev.mysql.com/worklog/task/?id=11808
2222
return self.connection.mysql_is_mariadb
23+
24+
@cached_property
25+
def django_test_skips(self):
26+
skips = super().django_test_skips
27+
if self.connection.mysql_is_mariadb:
28+
skips.update(
29+
{
30+
"MariaDB doesn't support nested geometry collections.": {
31+
"gis_tests.geoapp.tests.SaveLoadTests."
32+
"test_geometrycollectionfield_default_max_ignored_on_read",
33+
},
34+
}
35+
)
36+
return skips

django/contrib/gis/db/backends/mysql/operations.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ def get_geometry_converter(self, expression):
141141

142142
def converter(value, expression, connection):
143143
if value is not None:
144-
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
144+
geom = GEOSGeometryBase(
145+
read(memoryview(value), max_geom_collections=None), geom_class
146+
)
145147
if srid:
146148
geom.srid = srid
147149
return geom

django/contrib/gis/db/backends/oracle/features.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ def django_test_skips(self):
2323
"gis_tests.gis_migrations.test_operations.OperationTests."
2424
"test_add_check_constraint",
2525
},
26+
"Oracle doesn't support nested geometry collections.": {
27+
"gis_tests.geoapp.tests.SaveLoadTests."
28+
"test_geometrycollectionfield_default_max_ignored_on_read",
29+
},
2630
}
2731
)
2832
return skips

django/contrib/gis/db/backends/oracle/operations.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@ def get_geometry_converter(self, expression):
244244

245245
def converter(value, expression, connection):
246246
if value is not None:
247-
geom = GEOSGeometryBase(read(memoryview(value.read())), geom_class)
247+
geom = GEOSGeometryBase(
248+
read(memoryview(value.read()), max_geom_collections=None),
249+
geom_class,
250+
)
248251
if srid:
249252
geom.srid = srid
250253
return geom

django/contrib/gis/db/backends/postgis/operations.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,12 @@ def get_geometry_converter(self, expression):
422422
geom_class = expression.output_field.geom_class
423423

424424
def converter(value, expression, connection):
425-
if isinstance(value, str): # Coming from hex strings.
426-
value = value.encode("ascii")
427-
return None if value is None else GEOSGeometryBase(read(value), geom_class)
425+
if value is not None:
426+
if isinstance(value, str): # Coming from hex strings.
427+
value = value.encode("ascii")
428+
return GEOSGeometryBase(
429+
read(value, max_geom_collections=None), geom_class
430+
)
428431

429432
return converter
430433

django/contrib/gis/db/backends/spatialite/operations.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ def get_geometry_converter(self, expression):
230230
read = wkb_r().read
231231

232232
def converter(value, expression, connection):
233-
return None if value is None else GEOSGeometryBase(read(value), geom_class)
233+
if value is not None:
234+
return GEOSGeometryBase(
235+
read(value, max_geom_collections=None), geom_class
236+
)
234237

235238
return converter

django/contrib/gis/db/models/fields.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Point,
1818
Polygon,
1919
)
20+
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
2021
from django.core.exceptions import ImproperlyConfigured
2122
from django.db.models import Field
2223
from django.utils.translation import gettext_lazy as _
@@ -214,8 +215,11 @@ def get_prep_value(self, value):
214215
if raster:
215216
obj = raster
216217
elif is_candidate:
218+
max_geom_collections = getattr(
219+
self, "max_geom_collections", MAX_GEOM_COLLECTIONS
220+
)
217221
try:
218-
obj = GEOSGeometry(obj)
222+
obj = GEOSGeometry(obj, max_geom_collections=max_geom_collections)
219223
except (TypeError, ValueError) as err:
220224
if isinstance(obj, str) and obj.startswith(VSI_FILESYSTEM_PREFIX):
221225
raise blocked_err
@@ -259,6 +263,7 @@ def __init__(
259263
*,
260264
extent=(-180.0, -90.0, 180.0, 90.0),
261265
tolerance=0.05,
266+
max_geom_collections=MAX_GEOM_COLLECTIONS,
262267
**kwargs,
263268
):
264269
"""
@@ -277,6 +282,10 @@ def __init__(
277282
tolerance:
278283
Define the tolerance, in meters, to use for the geometry field
279284
entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.
285+
286+
max_geom_collections:
287+
The maximum number of geometry collections accepted before parsing is
288+
refused, forwarded to the form field.
280289
"""
281290
# Setting the dimension of the geometry field.
282291
self.dim = dim
@@ -289,6 +298,10 @@ def __init__(
289298
self._extent = extent
290299
self._tolerance = tolerance
291300

301+
# Limit on nested/total geometry collections, forwarded to the form
302+
# field to guard against crashes in GEOS from deeply nested input.
303+
self.max_geom_collections = max_geom_collections
304+
292305
super().__init__(verbose_name=verbose_name, **kwargs)
293306

294307
def deconstruct(self):
@@ -302,6 +315,8 @@ def deconstruct(self):
302315
kwargs["extent"] = self._extent
303316
if self._tolerance != 0.05:
304317
kwargs["tolerance"] = self._tolerance
318+
if self.max_geom_collections != MAX_GEOM_COLLECTIONS:
319+
kwargs["max_geom_collections"] = self.max_geom_collections
305320
return name, path, args, kwargs
306321

307322
def contribute_to_class(self, cls, name, **kwargs):
@@ -319,6 +334,7 @@ def formfield(self, **kwargs):
319334
"form_class": self.form_class,
320335
"geom_type": self.geom_type,
321336
"srid": self.srid,
337+
"max_geom_collections": self.max_geom_collections,
322338
**kwargs,
323339
}
324340
if self.dim > 2 and not getattr(

django/contrib/gis/db/models/proxy.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ def __get__(self, instance, cls=None):
4343
else:
4444
# Otherwise, a geometry or raster object is built using the field's
4545
# contents, and the model's corresponding attribute is set.
46-
geo_obj = self._load_func(geo_value)
46+
try:
47+
max_geoms = self.field.max_geom_collections
48+
except AttributeError:
49+
geo_obj = self._load_func(geo_value)
50+
else:
51+
geo_obj = self._load_func(geo_value, max_geom_collections=max_geoms)
4752
setattr(instance, self.field.attname, geo_obj)
4853
return geo_obj
4954

django/contrib/gis/forms/fields.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django import forms
22
from django.contrib.gis.geos import GEOSException, GEOSGeometry
3+
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
34
from django.core.exceptions import ValidationError
45
from django.utils.translation import gettext_lazy as _
56

@@ -15,6 +16,7 @@ class GeometryField(forms.Field):
1516

1617
widget = OpenLayersWidget
1718
geom_type = "GEOMETRY"
19+
max_geom_collections = MAX_GEOM_COLLECTIONS
1820

1921
default_error_messages = {
2022
"required": _("No geometry value provided."),
@@ -26,12 +28,20 @@ class GeometryField(forms.Field):
2628
),
2729
}
2830

29-
def __init__(self, *, srid=None, geom_type=None, **kwargs):
31+
def __init__(
32+
self, *, srid=None, geom_type=None, max_geom_collections=None, **kwargs
33+
):
3034
self.srid = srid
3135
if geom_type is not None:
3236
self.geom_type = geom_type
37+
if max_geom_collections is not None:
38+
self.max_geom_collections = max_geom_collections
3339
super().__init__(**kwargs)
3440
self.widget.attrs["geom_type"] = self.geom_type
41+
# Propagate the limit to the (per-field) widget instance, which does
42+
# the actual parsing. Custom widgets that override deserialize() and
43+
# ignore this attribute still get the default limit via GEOSGeometry.
44+
self.widget.max_geom_collections = self.max_geom_collections
3545

3646
def to_python(self, value):
3747
"""Transform the value to a Geometry object."""
@@ -43,7 +53,9 @@ def to_python(self, value):
4353
value = self.widget.deserialize(value)
4454
else:
4555
try:
46-
value = GEOSGeometry(value)
56+
value = GEOSGeometry(
57+
value, max_geom_collections=self.max_geom_collections
58+
)
4759
except (GEOSException, ValueError, TypeError):
4860
value = None
4961
if value is None:

django/contrib/gis/forms/widgets.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.gis.gdal import GDALException
55
from django.contrib.gis.geometry import json_regex
66
from django.contrib.gis.geos import GEOSException, GEOSGeometry
7+
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
78
from django.forms.widgets import Widget
89

910
logger = logging.getLogger("django.contrib.gis")
@@ -19,6 +20,7 @@ class BaseGeometryWidget(Widget):
1920
geom_type = "GEOMETRY"
2021
map_srid = 4326
2122
display_raw = False
23+
max_geom_collections = MAX_GEOM_COLLECTIONS
2224

2325
supports_3d = False
2426
template_name = "" # set on subclasses
@@ -36,7 +38,7 @@ def serialize(self, value):
3638

3739
def deserialize(self, value):
3840
try:
39-
return GEOSGeometry(value)
41+
return GEOSGeometry(value, max_geom_collections=self.max_geom_collections)
4042
except (GEOSException, GDALException, ValueError, TypeError) as err:
4143
logger.error("Error creating geometry from value '%s' (%s)", value, err)
4244
return None

0 commit comments

Comments
 (0)