티스토리 수익 글 보기

티스토리 수익 글 보기

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

Commit ba80833

Browse files
jacobtylerwallsnessita
authored andcommitted
[5.2.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 c72a5db commit ba80833

22 files changed

Lines changed: 653 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
@@ -19,3 +19,17 @@ class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures):
1919
def supports_geometry_field_unique_index(self):
2020
# Not supported in MySQL since https://dev.mysql.com/worklog/task/?id=11808
2121
return self.connection.mysql_is_mariadb
22+
23+
@cached_property
24+
def django_test_skips(self):
25+
skips = super().django_test_skips
26+
if self.connection.mysql_is_mariadb:
27+
skips.update(
28+
{
29+
"MariaDB doesn't support nested geometry collections.": {
30+
"gis_tests.geoapp.tests.SaveLoadTests."
31+
"test_geometrycollectionfield_default_max_ignored_on_read",
32+
},
33+
}
34+
)
35+
return skips

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

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

136136
def converter(value, expression, connection):
137137
if value is not None:
138-
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
138+
geom = GEOSGeometryBase(
139+
read(memoryview(value), max_geom_collections=None), geom_class
140+
)
139141
if srid:
140142
geom.srid = srid
141143
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
@@ -236,7 +236,10 @@ def get_geometry_converter(self, expression):
236236

237237
def converter(value, expression, connection):
238238
if value is not None:
239-
geom = GEOSGeometryBase(read(memoryview(value.read())), geom_class)
239+
geom = GEOSGeometryBase(
240+
read(memoryview(value.read()), max_geom_collections=None),
241+
geom_class,
242+
)
240243
if srid:
241244
geom.srid = srid
242245
return geom

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

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

421421
def converter(value, expression, connection):
422-
if isinstance(value, str): # Coming from hex strings.
423-
value = value.encode("ascii")
424-
return None if value is None else GEOSGeometryBase(read(value), geom_class)
422+
if value is not None:
423+
if isinstance(value, str): # Coming from hex strings.
424+
value = value.encode("ascii")
425+
return GEOSGeometryBase(
426+
read(value, max_geom_collections=None), geom_class
427+
)
425428

426429
return converter
427430

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

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

228228
def converter(value, expression, connection):
229-
return None if value is None else GEOSGeometryBase(read(value), geom_class)
229+
if value is not None:
230+
return GEOSGeometryBase(
231+
read(value, max_geom_collections=None), geom_class
232+
)
230233

231234
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 _
@@ -213,8 +214,11 @@ def get_prep_value(self, value):
213214
if raster:
214215
obj = raster
215216
elif is_candidate:
217+
max_geom_collections = getattr(
218+
self, "max_geom_collections", MAX_GEOM_COLLECTIONS
219+
)
216220
try:
217-
obj = GEOSGeometry(obj)
221+
obj = GEOSGeometry(obj, max_geom_collections=max_geom_collections)
218222
except (TypeError, ValueError) as err:
219223
if isinstance(obj, str) and obj.startswith(VSI_FILESYSTEM_PREFIX):
220224
raise blocked_err
@@ -258,6 +262,7 @@ def __init__(
258262
*,
259263
extent=(-180.0, -90.0, 180.0, 90.0),
260264
tolerance=0.05,
265+
max_geom_collections=MAX_GEOM_COLLECTIONS,
261266
**kwargs,
262267
):
263268
"""
@@ -276,6 +281,10 @@ def __init__(
276281
tolerance:
277282
Define the tolerance, in meters, to use for the geometry field
278283
entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.
284+
285+
max_geom_collections:
286+
The maximum number of geometry collections accepted before parsing is
287+
refused, forwarded to the form field.
279288
"""
280289
# Setting the dimension of the geometry field.
281290
self.dim = dim
@@ -288,6 +297,10 @@ def __init__(
288297
self._extent = extent
289298
self._tolerance = tolerance
290299

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

293306
def deconstruct(self):
@@ -301,6 +314,8 @@ def deconstruct(self):
301314
kwargs["extent"] = self._extent
302315
if self._tolerance != 0.05:
303316
kwargs["tolerance"] = self._tolerance
317+
if self.max_geom_collections != MAX_GEOM_COLLECTIONS:
318+
kwargs["max_geom_collections"] = self.max_geom_collections
304319
return name, path, args, kwargs
305320

306321
def contribute_to_class(self, cls, name, **kwargs):
@@ -318,6 +333,7 @@ def formfield(self, **kwargs):
318333
"form_class": self.form_class,
319334
"geom_type": self.geom_type,
320335
"srid": self.srid,
336+
"max_geom_collections": self.max_geom_collections,
321337
**kwargs,
322338
}
323339
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
@@ -4,6 +4,7 @@
44
from django.contrib.gis import gdal
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
from django.utils import translation
910

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