Opened 83 minutes ago
#37282 assigned New feature
ModelForm shouldn't fallback to forms.CharField for RasterField
| Reported by: | Jacob Walls | Owned by: | Django Sprints |
|---|---|---|---|
| Component: | GIS | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Recent security reports around raster objects pointed out that ModelForm will fallback to forms.CharField by default, e.g. in the admin.
After the effort we went to in f1949c1f9758947ade984c895ff16bef46f56520 to advise folks to write custom validation when accepting raster definitions from untrusted user input, we could enforce this by refusing to let ModelForm fallback to CharField.
My first idea is to simply register a RasterField that raises. (Or raises after a deprecation, but we can also consider this a "security feature" that ModelForm no longer falls back to something clearly inadequate and just get it in for 6.2? The API stability policy contemplates security as an exception.)
Sketch:
-
django/contrib/gis/db/models/fields.py
diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py index a001c9a720..3da930a0d8 100644
a b class RasterField(BaseSpatialField): 423 423 """ 424 424 425 425 description = _("Raster Field") 426 form_class = forms.RasterField 426 427 geom_type = "RASTER" 427 428 geography = False 428 429 … … class RasterField(BaseSpatialField): 452 453 # of the raster attribute. 453 454 setattr(cls, self.attname, SpatialProxy(gdal.GDALRaster, self)) 454 455 456 def formfield(self, **kwargs): 457 return super().formfield( 458 **{ 459 "form_class": self.form_class, 460 **kwargs, 461 } 462 ) 463 455 464 def get_transform(self, name): 456 465 from django.contrib.gis.db.models.lookups import RasterBandTransform 457 466 -
django/contrib/gis/forms/__init__.py
diff --git a/django/contrib/gis/forms/__init__.py b/django/contrib/gis/forms/__init__.py index c07720b2d0..29d20bed8a 100644
a b from .fields import ( # NOQA 9 9 MultiPolygonField, 10 10 PointField, 11 11 PolygonField, 12 RasterField, 12 13 ) 13 14 from .widgets import BaseGeometryWidget, OpenLayersWidget, OSMWidget # NOQA -
django/contrib/gis/forms/fields.py
diff --git a/django/contrib/gis/forms/fields.py b/django/contrib/gis/forms/fields.py index dcc8bb219e..a91e0def76 100644
a b from django.utils.translation import gettext_lazy as _ 7 7 from .widgets import OpenLayersWidget 8 8 9 9 10 class RasterField(forms.Field): 11 geom_type = "RASTER" 12 13 def __init__(self, *args, **kwargs): 14 raise NotImplementedError("Security: do your own validation...!") 15 16 10 17 class GeometryField(forms.Field): 11 18 """ 12 19 This is the basic form field for a Geometry. Any textual input that is -
tests/gis_tests/rasterapp/test_rasterfield.py
diff --git a/tests/gis_tests/rasterapp/test_rasterfield.py b/tests/gis_tests/rasterapp/test_rasterfield.py index af0bcd2c20..09527a8cc1 100644
a b 1 1 import json 2 2 from unittest import mock 3 3 4 from django.contrib import admin 4 5 from django.contrib.gis.db.models.fields import BaseSpatialField 5 6 from django.contrib.gis.db.models.functions import Distance 6 7 from django.contrib.gis.db.models.lookups import ( … … from django.contrib.gis.measure import D 14 15 from django.contrib.gis.shortcuts import numpy 15 16 from django.db import connection 16 17 from django.db.models import F, Func, Q 17 from django.test import T ransactionTestCase, skipUnlessDBFeature18 from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature 18 19 from django.test.utils import CaptureQueriesContext 19 20 20 21 from ..data.rasters.textrasters import JSON_RASTER 21 22 from .models import RasterModel, RasterRelatedModel 22 23 23 24 25 site = admin.AdminSite(name="rasterapp_modeladmin") 26 site.register(RasterModel, admin.ModelAdmin) 27 28 24 29 @skipUnlessDBFeature("supports_raster") 25 30 class RasterFieldTest(TransactionTestCase): 26 31 available_apps = ["gis_tests.rasterapp"] … … class RasterFieldTest(TransactionTestCase): 502 507 # It's easier to check the indexes in the generated SQL than to write 503 508 # tests that cover all index combinations. 504 509 self.assertRegex(queries[-1]["sql"], r"WHERE ST_Contains\([^)]*, 1, [^)]*, 1\)") 510 511 512 @skipUnlessDBFeature("supports_raster") 513 class RasterFieldAdminTest(TestCase): 514 def test_form_raises(self): 515 geoadmin = site.get_model_admin(RasterModel) 516 with self.assertRaises(NotImplementedError): 517 geoadmin.get_changelist_form(None)() -
tests/gis_tests/test_geoforms.py
diff --git a/tests/gis_tests/test_geoforms.py b/tests/gis_tests/test_geoforms.py index 3336f1e68d..da8139d891 100644
a b from django.utils.html import escape 12 12 from .data.rasters.textrasters import JSON_RASTER 13 13 14 14 15 class RasterFieldTest(SimpleTestCase): 16 ... 17 18 15 19 class GeometryFieldTest(SimpleTestCase): 16 20 def test_init(self): 17 21 "Testing GeometryField initialization with defaults."