티스토리 수익 글 보기

티스토리 수익 글 보기

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

Commit 6af5da3

Browse files
jacobtylerwallsnessita
authored andcommitted
[6.0.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 224dbc8 commit 6af5da3

23 files changed

Lines changed: 673 additions & 31 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
@@ -142,7 +142,9 @@ def get_geometry_converter(self, expression):
142142

143143
def converter(value, expression, connection):
144144
if value is not None:
145-
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
145+
geom = GEOSGeometryBase(
146+
read(memoryview(value), max_geom_collections=None), geom_class
147+
)
146148
if srid:
147149
geom.srid = srid
148150
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
@@ -243,7 +243,10 @@ def get_geometry_converter(self, expression):
243243

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

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

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

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

430433
return converter
431434

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

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

230230
def converter(value, expression, connection):
231-
return None if value is None else GEOSGeometryBase(read(value), geom_class)
231+
if value is not None:
232+
return GEOSGeometryBase(
233+
read(value, max_geom_collections=None), geom_class
234+
)
232235

233236
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,6 +1,7 @@
11
from django import forms
22
from django.contrib.gis.gdal import GDALException
33
from django.contrib.gis.geos import GEOSException, GEOSGeometry
4+
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
45
from django.core.exceptions import ValidationError
56
from django.utils.translation import gettext_lazy as _
67

@@ -16,6 +17,7 @@ class GeometryField(forms.Field):
1617

1718
widget = OpenLayersWidget
1819
geom_type = "GEOMETRY"
20+
max_geom_collections = MAX_GEOM_COLLECTIONS
1921

2022
default_error_messages = {
2123
"required": _("No geometry value provided."),
@@ -27,12 +29,20 @@ class GeometryField(forms.Field):
2729
),
2830
}
2931

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

3747
def to_python(self, value):
3848
"""Transform the value to a Geometry object."""
@@ -47,7 +57,9 @@ def to_python(self, value):
4757
value = None
4858
else:
4959
try:
50-
value = GEOSGeometry(value)
60+
value = GEOSGeometry(
61+
value, max_geom_collections=self.max_geom_collections
62+
)
5163
except (GEOSException, ValueError, TypeError):
5264
value = None
5365
if value is None:

django/contrib/gis/forms/widgets.py

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

89
logger = logging.getLogger("django.contrib.gis")
@@ -18,6 +19,7 @@ class BaseGeometryWidget(Widget):
1819
geom_type = "GEOMETRY"
1920
map_srid = 4326
2021
display_raw = False
22+
max_geom_collections = MAX_GEOM_COLLECTIONS
2123

2224
supports_3d = False
2325
template_name = "" # set on subclasses
@@ -35,7 +37,7 @@ def serialize(self, value):
3537

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

0 commit comments

Comments
 (0)