#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):  
    423423    """
    424424
    425425    description = _("Raster Field")
     426    form_class = forms.RasterField
    426427    geom_type = "RASTER"
    427428    geography = False
    428429
    class RasterField(BaseSpatialField):  
    452453        # of the raster attribute.
    453454        setattr(cls, self.attname, SpatialProxy(gdal.GDALRaster, self))
    454455
     456    def formfield(self, **kwargs):
     457        return super().formfield(
     458            **{
     459                "form_class": self.form_class,
     460                **kwargs,
     461            }
     462        )
     463
    455464    def get_transform(self, name):
    456465        from django.contrib.gis.db.models.lookups import RasterBandTransform
    457466
  • 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  
    99    MultiPolygonField,
    1010    PointField,
    1111    PolygonField,
     12    RasterField,
    1213)
    1314from .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 _  
    77from .widgets import OpenLayersWidget
    88
    99
     10class RasterField(forms.Field):
     11    geom_type = "RASTER"
     12
     13    def __init__(self, *args, **kwargs):
     14        raise NotImplementedError("Security: do your own validation...!")
     15
     16
    1017class GeometryField(forms.Field):
    1118    """
    1219    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  
    11import json
    22from unittest import mock
    33
     4from django.contrib import admin
    45from django.contrib.gis.db.models.fields import BaseSpatialField
    56from django.contrib.gis.db.models.functions import Distance
    67from django.contrib.gis.db.models.lookups import (
    from django.contrib.gis.measure import D  
    1415from django.contrib.gis.shortcuts import numpy
    1516from django.db import connection
    1617from django.db.models import F, Func, Q
    17 from django.test import TransactionTestCase, skipUnlessDBFeature
     18from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
    1819from django.test.utils import CaptureQueriesContext
    1920
    2021from ..data.rasters.textrasters import JSON_RASTER
    2122from .models import RasterModel, RasterRelatedModel
    2223
    2324
     25site = admin.AdminSite(name="rasterapp_modeladmin")
     26site.register(RasterModel, admin.ModelAdmin)
     27
     28
    2429@skipUnlessDBFeature("supports_raster")
    2530class RasterFieldTest(TransactionTestCase):
    2631    available_apps = ["gis_tests.rasterapp"]
    class RasterFieldTest(TransactionTestCase):  
    502507        # It's easier to check the indexes in the generated SQL than to write
    503508        # tests that cover all index combinations.
    504509        self.assertRegex(queries[-1]["sql"], r"WHERE ST_Contains\([^)]*, 1, [^)]*, 1\)")
     510
     511
     512@skipUnlessDBFeature("supports_raster")
     513class 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  
    1212from .data.rasters.textrasters import JSON_RASTER
    1313
    1414
     15class RasterFieldTest(SimpleTestCase):
     16    ...
     17
     18
    1519class GeometryFieldTest(SimpleTestCase):
    1620    def test_init(self):
    1721        "Testing GeometryField initialization with defaults."

Change History (0)

Note: See TracTickets for help on using tickets.
Back to Top