diff mbox series

[meta-python,scarthgap,1/3] python3-django: fix CVE-2026-15307

Message ID 20260826052654.723156-1-dkelaiya@cisco.com
State New
Headers show
Series [meta-python,scarthgap,1/3] python3-django: fix CVE-2026-15307 | expand

Commit Message

From: Darsh Kelaiya <dkelaiya@cisco.com>

This patch applies the upstream fix as referenced in [2],
using the commit shown in [1].

[1] https://github.com/django/django/commit/115ffd0463a765ab1cc93de18e94b5459b8a300e
[2] https://nvd.nist.gov/vuln/detail/CVE-2026-15307

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
 .../CVE-2026-15307.patch                      | 563 ++++++++++++++++++
 .../python/python3-django_5.0.14.bb           |   1 +
 2 files changed, 564 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-django-5.0.14/CVE-2026-15307.patch
diff mbox series

Patch

diff --git a/meta-python/recipes-devtools/python/python3-django-5.0.14/CVE-2026-15307.patch b/meta-python/recipes-devtools/python/python3-django-5.0.14/CVE-2026-15307.patch
new file mode 100644
index 0000000000..ce45e92faf
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-django-5.0.14/CVE-2026-15307.patch
@@ -0,0 +1,563 @@ 
+From 1776209a7609053b6afe6cb0f14960c478de5424 Mon Sep 17 00:00:00 2001
+From: Jacob Walls <jacobtylerwalls@gmail.com>
+Date: Thu, 9 Jul 2026 11:07:28 -0400
+Subject: [PATCH] [5.2.x] Fixed CVE-2026-15307 -- Blocked raster strings and
+ dicts in spatial lookups.
+
+Spatial lookups optimistically parse values as rasters before retrying
+as geometries. If a malicious value reached the GDALRaster constructor,
+depending on the raster driver, it might write to disk or fetch from the
+network regardless of the constructor's `write=False` default argument.
+
+Although this works as designed for model field assignment, this is
+potentially unexpected for querying, for example, in the admin's
+changelist view, which allows staff users to execute arbitrary lookups
+on models registered with the admin.
+
+Network rasters didn't even work in lookup contexts before, providing
+further evidence that this use case was unintentional. (The failure
+point was after the fetching, however.)
+
+Now, strings and dicts representing rasters are rejected by spatial
+lookups. To opt in to using them, wrap them in a `GDALRaster` first.
+
+Although it would simplify the implementation to try geometries before
+rasters (instead of stashing a raster exception and raising it later),
+we maintain the current order, which has been stable for a decade.
+
+Thanks Bence Nagy, localhost-detect, and kimchunbok_ for providing
+information useful in evaluating this report. Thanks Simon Charette,
+Natalia Bidart, and Sarah Boyce for reviews.
+
+Backport of f1949c1f9758947ade984c895ff16bef46f56520 from main.
+
+CVE: CVE-2026-15307
+Upstream-Status: Backport [https://github.com/django/django/commit/115ffd0463a765ab1cc93de18e94b5459b8a300e]
+
+Backport Changes:
+- Dropped the file docs/releases/5.2.17.txt as
+  current version for Scarthgap is 5.0.14
+
+(cherry picked from commit 115ffd0463a765ab1cc93de18e94b5459b8a300e)
+Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
+---
+ django/contrib/gis/db/models/fields.py        | 37 +++++---
+ django/contrib/gis/gdal/raster/source.py      | 46 ++++++++--
+ docs/ref/contrib/gis/db-api.txt               | 12 ++-
+ docs/ref/contrib/gis/gdal.txt                 | 34 +++++++
+ tests/gis_tests/geoadmin/tests.py             | 25 ++++-
+ tests/gis_tests/geoapp/tests.py               | 91 +++++++++++++++++++
+ tests/gis_tests/rasterapp/test_rasterfield.py | 25 ++---
+ tests/gis_tests/test_geoforms.py              | 15 +++
+ 8 files changed, 246 insertions(+), 39 deletions(-)
+
+diff --git a/django/contrib/gis/db/models/fields.py b/django/contrib/gis/db/models/fields.py
+index 889c1cfe84..15d9ae3c57 100644
+--- a/django/contrib/gis/db/models/fields.py
++++ b/django/contrib/gis/db/models/fields.py
+@@ -3,6 +3,9 @@ from collections import defaultdict, namedtuple
+ from django.contrib.gis import forms, gdal
+ from django.contrib.gis.db.models.proxy import SpatialProxy
+ from django.contrib.gis.gdal.error import GDALException
++from django.contrib.gis.gdal.raster.const import VSI_FILESYSTEM_PREFIX
++from django.contrib.gis.gdal.raster.source import DisallowedRasterLookup
++from django.contrib.gis.geometry import json_regex
+ from django.contrib.gis.geos import (
+     GeometryCollection,
+     GEOSException,
+@@ -172,21 +175,19 @@ class BaseSpatialField(Field):
+     def get_raster_prep_value(self, value, is_candidate):
+         """
+         Return a GDALRaster if conversion is successful, otherwise return None.
++
++        Unless the user opts in by wrapping values in a GDALRaster, raise
++        DisallowedRasterLookup for values that fetch or write to disk.
+         """
+         if isinstance(value, gdal.GDALRaster):
+             return value
+-        elif is_candidate:
++        gdal.GDALRaster.check_raster_lookup_value(value)
++        if is_candidate:
+             try:
+                 return gdal.GDALRaster(value)
+             except GDALException:
+                 pass
+-        elif isinstance(value, dict):
+-            try:
+-                return gdal.GDALRaster(value)
+-            except GDALException:
+-                raise ValueError(
+-                    "Couldn't create spatial object from lookup value '%s'." % value
+-                )
++        return None
+ 
+     def get_prep_value(self, value):
+         obj = super().get_prep_value(value)
+@@ -202,22 +203,36 @@ class BaseSpatialField(Field):
+                 obj, "__geo_interface__"
+             )
+             # Try to convert the input to raster.
+-            raster = self.get_raster_prep_value(obj, is_candidate)
+-
++            raster = None
++            blocked_err = None
++            try:
++                raster = self.get_raster_prep_value(obj, is_candidate)
++            except DisallowedRasterLookup as err:
++                if isinstance(obj, dict):
++                    raise err
++                # Don't immediately raise in case this is a valid GEOSGeometry.
++                blocked_err = err
+             if raster:
+                 obj = raster
+             elif is_candidate:
+                 try:
+                     obj = GEOSGeometry(obj)
++                except (TypeError, ValueError) as err:
++                    if isinstance(obj, str) and obj.startswith(VSI_FILESYSTEM_PREFIX):
++                        raise blocked_err
++                    raise err
+                 except (GEOSException, GDALException):
++                    if isinstance(obj, str) and json_regex.match(obj):
++                        raise blocked_err
+                     raise ValueError(
+                         "Couldn't create spatial object from lookup value '%s'." % obj
+                     )
+             else:
+-                raise ValueError(
++                msg = (
+                     "Cannot use object with type %s for a spatial lookup parameter."
+                     % type(obj).__name__
+                 )
++                raise blocked_err or ValueError(msg)
+ 
+         # Assigning the SRID value.
+         obj.srid = self.get_srid(obj)
+diff --git a/django/contrib/gis/gdal/raster/source.py b/django/contrib/gis/gdal/raster/source.py
+index b33eb11c0f..f63e7d0f30 100644
+--- a/django/contrib/gis/gdal/raster/source.py
++++ b/django/contrib/gis/gdal/raster/source.py
+@@ -28,10 +28,19 @@ from django.contrib.gis.gdal.raster.const import (
+ )
+ from django.contrib.gis.gdal.srs import SpatialReference, SRSException
+ from django.contrib.gis.geometry import json_regex
++from django.core.exceptions import SuspiciousOperation
+ from django.utils.encoding import force_bytes, force_str
+ from django.utils.functional import cached_property
+ 
+ 
++class DisallowedRasterLookup(SuspiciousOperation):
++    """
++    Types that force GDALRaster to open in write mode (dict) or values that
++    could be virtual filesystem paths (str) are not allowed in lookup contexts.
++    Instead, wrap values in GDALRaster explicitly.
++    """
++
++
+ class TransformPoint(list):
+     indices = {
+         "origin": (0, 3),
+@@ -78,14 +87,10 @@ class GDALRaster(GDALRasterBase):
+         self._write = 1 if write else 0
+         Driver.ensure_registered()
+ 
+-        # Preprocess json inputs. This converts json strings to dictionaries,
+-        # which are parsed below the same way as direct dictionary inputs.
+-        if isinstance(ds_input, str) and json_regex.match(ds_input):
+-            ds_input = json.loads(ds_input)
++        ds_input = self._preprocess_input(ds_input)
+ 
+         # If input is a valid file path, try setting file as source.
+-        if isinstance(ds_input, (str, Path)):
+-            ds_input = str(ds_input)
++        if isinstance(ds_input, str):
+             if not ds_input.startswith(VSI_FILESYSTEM_PREFIX) and not os.path.exists(
+                 ds_input
+             ):
+@@ -226,6 +231,35 @@ class GDALRaster(GDALRasterBase):
+         """
+         return "<Raster object at %s>" % hex(addressof(self._ptr))
+ 
++    @classmethod
++    def _preprocess_input(cls, ds_input):
++        """
++        Preprocess json and Path inputs. This converts json strings to
++        dictionaries, which are then parsed just like direct dictionary inputs.
++        This also stringifies Path objects.
++        """
++        if isinstance(ds_input, str) and json_regex.match(ds_input):
++            ds_input = json.loads(ds_input)
++        if isinstance(ds_input, Path):
++            ds_input = str(ds_input)
++        return ds_input
++
++    @classmethod
++    def check_raster_lookup_value(cls, ds_input):
++        """
++        Raise DisallowedRasterLookup for values inappropriate in lookups:
++        - No dicts, which GDALRaster(write=False) might still write to.
++        - No strings or Paths, which might fetch over the virtual filesystem.
++        """
++        normalized = cls._preprocess_input(ds_input)
++        if isinstance(normalized, (dict, str)):
++            msg = (
++                f"Cannot use object {normalized!r} for a spatial lookup "
++                "parameter. If this is a raster, wrap it with GDALRaster() "
++                "before using it in a lookup to enable writing or fetching."
++            )
++            raise DisallowedRasterLookup(msg)
++
+     def _flush(self):
+         """
+         Flush all data from memory into the source file if it exists.
+diff --git a/docs/ref/contrib/gis/db-api.txt b/docs/ref/contrib/gis/db-api.txt
+index df1d3847e6..51dece9b63 100644
+--- a/docs/ref/contrib/gis/db-api.txt
++++ b/docs/ref/contrib/gis/db-api.txt
+@@ -146,11 +146,21 @@ GeoDjango are only available on spatial fields.
+ 
+ Filters on 'normal' fields (e.g. :class:`~django.db.models.CharField`)
+ may be chained with those on geographic fields. Geographic lookups accept
+-geometry and raster input on both sides and input types can be mixed freely.
++geometry and raster input on both sides, and input types can be mixed freely in
++most cases. However, unlike assignments to model fields, with lookups,
++types such as ``str``, :class:`pathlib.Path`, and ``dict`` must be wrapped by
++:class:`~django.contrib.gis.gdal.GDALRaster` to signify that the potential for
++file writing or network fetching is acceptable. For the rationale, see
++:ref:`raster security considerations <raster-security>`.
+ 
+ The general structure of geographic lookups is described below. A complete
+ reference can be found in the :ref:`spatial lookup reference<spatial-lookups>`.
+ 
++.. versionchanged:: 5.2.17
++
++    In earlier versions, spatial lookups accepted ``str`` and ``dict`` types
++    for new rasters, allowing file writes and network fetches.
++
+ Geometry Lookups
+ ----------------
+ 
+diff --git a/docs/ref/contrib/gis/gdal.txt b/docs/ref/contrib/gis/gdal.txt
+index 9011aa6e2b..2df807ee74 100644
+--- a/docs/ref/contrib/gis/gdal.txt
++++ b/docs/ref/contrib/gis/gdal.txt
+@@ -2068,6 +2068,40 @@ previously configured for authentication and possibly other settings (see the
+ 
+ .. _`GDAL Virtual Filesystems documentation`: https://gdal.org/user/virtual_file_systems.html
+ 
++.. _raster-security:
++
++Security considerations
++~~~~~~~~~~~~~~~~~~~~~~~
++
++Since :class:`GDALRaster` always opens new rasters in write mode, it is
++essential to prevent instantiating one from untrusted input. Otherwise, an
++attacker might gain the ability to write a file or make a network request.
++
++To mitigate this, :ref:`spatial lookups <spatial-lookups-intro>` prevent
++``str``, :class:`pathlib.Path`, and ``dict`` values from reaching
++:class:`GDALRaster` altogether. To use these types with lookups, wrap them
++explicitly with :class:`GDALRaster`, indicating that the value is trusted.
++Bytes are accepted without being wrapped in :class:`GDALRaster` because they
++are opened through GDAL's memory-based :ref:`virtual filesystem
++<gdal-raster-vsimem>`.
++
++This protection applies only to spatial lookups. Assigning a ``dict`` value to
++a :class:`~django.contrib.gis.db.models.RasterField` will still open a new
++raster, and assigning a ``str`` or ``Path`` will still fetch and open the
++referenced raster.
++
++When validating geometry inputs, the
++:class:`~django.contrib.gis.forms.GeometryField` form field will reject raster
++values. When validating raster inputs, you should write custom validation.
++
++For defense-in-depth strategies for limiting the available raster drivers, see
++`GDAL security considerations <https://gdal.org/user/security.html>`_.
++
++.. versionchanged:: 5.2.17
++
++    In earlier versions, spatial lookups accepted ``str`` and ``dict`` types
++    for new rasters, allowing file writes and network fetches.
++
+ Settings
+ ========
+ 
+diff --git a/tests/gis_tests/geoadmin/tests.py b/tests/gis_tests/geoadmin/tests.py
+index e101050464..2db36b49de 100644
+--- a/tests/gis_tests/geoadmin/tests.py
++++ b/tests/gis_tests/geoadmin/tests.py
+@@ -1,13 +1,26 @@
++from django.contrib.auth.models import Permission, User
++from django.contrib.contenttypes.models import ContentType
+ from django.contrib.gis.geos import Point
+-from django.test import SimpleTestCase, override_settings
++from django.core.exceptions import SuspiciousOperation
++from django.test import RequestFactory, TestCase, override_settings
+ 
+ from .models import City, site, site_gis, site_gis_custom
+ 
+ 
+ @override_settings(ROOT_URLCONF="django.contrib.gis.tests.geoadmin.urls")
+-class GeoAdminTest(SimpleTestCase):
++class GeoAdminTest(TestCase):
+     admin_site = site  # ModelAdmin
+ 
++    @classmethod
++    def setUpTestData(cls):
++        cls.user = User.objects.create_user("test", password="password", is_staff=True)
++        cls.user.user_permissions.add(
++            Permission.objects.get(
++                codename="view_city",
++                content_type=ContentType.objects.get_for_model(City),
++            )
++        )
++
+     def test_widget_empty_string(self):
+         geoadmin = self.admin_site.get_model_admin(City)
+         form = geoadmin.get_changelist_form(None)({"point": ""})
+@@ -54,6 +67,14 @@ class GeoAdminTest(SimpleTestCase):
+         self.assertIs(has_changed(initial, data_almost_same), False)
+         self.assertIs(has_changed(initial, data_changed), True)
+ 
++    def test_raster_lookup_not_allowed(self):
++        geoadmin = self.admin_site.get_model_admin(City)
++        request = RequestFactory().get("/city/", data={"point": "/vsicurl/someurl"})
++        request.user = self.user
++        msg = "Cannot use object '/vsicurl/someurl' for a spatial lookup parameter."
++        with self.assertRaisesMessage(SuspiciousOperation, msg):
++            geoadmin.get_changelist_instance(request)
++
+ 
+ class GISAdminTests(GeoAdminTest):
+     admin_site = site_gis  # GISModelAdmin
+diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py
+index 7ee47ee9a8..6be13d4907 100644
+--- a/tests/gis_tests/geoapp/tests.py
++++ b/tests/gis_tests/geoapp/tests.py
+@@ -1,7 +1,10 @@
++import json
+ from io import StringIO
++from pathlib import Path
+ 
+ from django.contrib.gis import gdal
+ from django.contrib.gis.db.models import Extent, MakeLine, Union, functions
++from django.contrib.gis.gdal.raster.source import DisallowedRasterLookup
+ from django.contrib.gis.geos import (
+     GeometryCollection,
+     GEOSGeometry,
+@@ -21,6 +24,7 @@ from django.db.models import F, OuterRef, Subquery
+ from django.test import TestCase, skipUnlessDBFeature
+ from django.test.utils import CaptureQueriesContext
+ 
++from ..data.rasters.textrasters import JSON_RASTER
+ from ..utils import skipUnlessGISLookup
+ from .models import (
+     City,
+@@ -598,6 +602,93 @@ class GeoLookupTest(TestCase):
+         )
+         self.assertEqual(qs.get(), multifields)
+ 
++    def test_lookup_rejects_writing_or_fetching_rasters(self):
++        """
++        GDALRaster enables write mode in the following cases even when the
++        value of the `write` parameter is False (default):
++        - dicts
++        - strings matching a json regex
++        - bytes
++
++        Since this could be unexpected in a lookup context, disallow dicts
++        and strings: instead, explicitly wrap with GDALRaster() to signal that
++        a write or fetch is expected. Bytes only write to the in-memory virtual
++        filesystem, so allow them.
++
++        Disallowing strings also disallows paths to local or network rasters,
++        but those didn't work in the lookup context anyway, since they were
++        never opened for writing, and lookups failed on setting the SRID with:
++
++        GDALException: Raster needs to be opened in write mode to change values
++
++        Still, a network fetch might have occurred before that failure point,
++        so disallow strings altogether.
++        """
++        # Create a vsi-based raster from scratch.
++        vsimem_path = "/vsimem/raster.tif"
++        # Keep a reference to this raster while it is being re-parsed below.
++        # Otherwise, GDALRaster.__del__() will delete the in-memory raster.
++        _rast = gdal.GDALRaster(  # NOQA: F841
++            {
++                "name": vsimem_path,
++                "driver": "tif",
++                "width": 4,
++                "height": 4,
++                "srid": 4326,
++                "bands": [
++                    {
++                        "data": range(16),
++                    }
++                ],
++            }
++        )
++        existing_path = Path(__file__).parent.parent / "data" / "rasters" / "raster.tif"
++        disallowed_cases = [
++            JSON_RASTER,
++            json.loads(JSON_RASTER),
++            "/vsicurl/someurl",
++            "/vsicurl_streaming/someurl",
++            "/vsis3/someurl",
++            vsimem_path,
++            existing_path,
++        ]
++        for obj in disallowed_cases:
++            try:
++                msg_obj = json.loads(obj)
++            except Exception:
++                if isinstance(obj, Path):
++                    msg_obj = str(obj)
++                else:
++                    msg_obj = obj
++            msg = (
++                f"Cannot use object {msg_obj!r} for a spatial lookup parameter. "
++                "If this is a raster, wrap it with GDALRaster() before using "
++                "it in a lookup to enable writing or fetching."
++            )
++            with (
++                self.subTest(obj=obj),
++                self.assertRaisesMessage(DisallowedRasterLookup, msg),
++            ):
++                City.objects.filter(point__contained=obj)
++
++        # Strings having nothing to do with rasters raise a more generic error.
++        for obj in str(existing_path), "invalid":
++            msg = "String input unrecognized as WKT EWKT, and HEXEWKB."
++            with self.subTest(obj=obj), self.assertRaisesMessage(ValueError, msg):
++                City.objects.filter(point__contained=obj)
++
++    def test_lookup_allows_writing_raster_from_bytes(self):
++        raster_path = Path(__file__).parent.parent / "data" / "rasters" / "raster.tif"
++        with open(raster_path, "rb") as raster_file:
++            raster_bytes = raster_file.read()
++        # Just get SQL to avoid gating on connection.supports_raster.
++        City.objects.filter(point__contained=raster_bytes).query
++
++    def test_lookup_allows_geos_geometry_string(self):
++        geojson = json.dumps({"type": "Point", "coordinates": [2, 49]})
++        # Just get SQL to avoid gating on connection.supports_raster.
++        City.objects.filter(point__contained=geojson).query
++
+ 
+ class GeoQuerySetTest(TestCase):
+     # TODO: GeoQuerySet is removed, organize these test better.
+diff --git a/tests/gis_tests/rasterapp/test_rasterfield.py b/tests/gis_tests/rasterapp/test_rasterfield.py
+index 3f2ce770a9..37eec50027 100644
+--- a/tests/gis_tests/rasterapp/test_rasterfield.py
++++ b/tests/gis_tests/rasterapp/test_rasterfield.py
+@@ -207,7 +207,7 @@ class RasterFieldTest(TransactionTestCase):
+                     (stx_pnt, 0, 500),
+                     (stx_pnt, D(km=1000)),
+                     (rast, 500),
+-                    (json.loads(JSON_RASTER), 500),
++                    (GDALRaster(json.loads(JSON_RASTER)), 500),
+                 ]
+             elif name == "relate":
+                 # Set lookup values for the relate lookup.
+@@ -218,7 +218,7 @@ class RasterFieldTest(TransactionTestCase):
+                     (stx_pnt, 0, "T*T***FF*"),
+                     (stx_pnt, "T*T***FF*"),
+                     (rast, "T*T***FF*"),
+-                    (json.loads(JSON_RASTER), "T*T***FF*"),
++                    (GDALRaster(json.loads(JSON_RASTER)), "T*T***FF*"),
+                 ]
+             elif name == "isvalid":
+                 # The isvalid lookup doesn't make sense for rasters.
+@@ -232,7 +232,7 @@ class RasterFieldTest(TransactionTestCase):
+                     (stx_pnt, 0),
+                     stx_pnt,
+                     rast,
+-                    json.loads(JSON_RASTER),
++                    GDALRaster(json.loads(JSON_RASTER)),
+                 ]
+             else:
+                 # Override band lookup for these, as it's not supported.
+@@ -245,7 +245,7 @@ class RasterFieldTest(TransactionTestCase):
+                     stx_pnt,
+                     stx_pnt,
+                     rast,
+-                    json.loads(JSON_RASTER),
++                    GDALRaster(json.loads(JSON_RASTER)),
+                 ]
+ 
+             # Create query filter combinations.
+@@ -287,14 +287,6 @@ class RasterFieldTest(TransactionTestCase):
+         qs = RasterModel.objects.filter(rastprojected__dwithin=(rast, D(km=1)))
+         self.assertEqual(qs.count(), 1)
+ 
+-        qs = RasterModel.objects.filter(
+-            rastprojected__dwithin=(json.loads(JSON_RASTER), D(km=1))
+-        )
+-        self.assertEqual(qs.count(), 1)
+-
+-        qs = RasterModel.objects.filter(rastprojected__dwithin=(JSON_RASTER, D(km=1)))
+-        self.assertEqual(qs.count(), 1)
+-
+         # Filter in an unprojected coordinate system.
+         qs = RasterModel.objects.filter(rast__dwithin=(rast, 40))
+         self.assertEqual(qs.count(), 1)
+@@ -414,13 +406,8 @@ class RasterFieldTest(TransactionTestCase):
+         self.assertEqual(qs.count(), 0)
+ 
+     def test_lookup_value_error(self):
+-        # Test with invalid dict lookup parameter
+-        obj = {}
+-        msg = "Couldn't create spatial object from lookup value '%s'." % obj
+-        with self.assertRaisesMessage(ValueError, msg):
+-            RasterModel.objects.filter(geom__intersects=obj)
+         # Test with invalid string lookup parameter
+-        obj = "00000"
++        obj = "POINT()"
+         msg = "Couldn't create spatial object from lookup value '%s'." % obj
+         with self.assertRaisesMessage(ValueError, msg):
+             RasterModel.objects.filter(geom__intersects=obj)
+@@ -449,7 +436,7 @@ class RasterFieldTest(TransactionTestCase):
+     def test_lhs_with_index_rhs_without_index(self):
+         with CaptureQueriesContext(connection) as queries:
+             RasterModel.objects.filter(
+-                rast__0__contains=json.loads(JSON_RASTER)
++                rast__0__contains=GDALRaster(json.loads(JSON_RASTER))
+             ).exists()
+         # It's easier to check the indexes in the generated SQL than to write
+         # tests that cover all index combinations.
+diff --git a/tests/gis_tests/test_geoforms.py b/tests/gis_tests/test_geoforms.py
+index b8105645bf..b980892790 100644
+--- a/tests/gis_tests/test_geoforms.py
++++ b/tests/gis_tests/test_geoforms.py
+@@ -8,6 +8,8 @@ from django.test import SimpleTestCase, override_settings
+ from django.utils.deprecation import RemovedInDjango51Warning
+ from django.utils.html import escape
+ 
++from .data.rasters.textrasters import JSON_RASTER
++
+ 
+ class GeometryFieldTest(SimpleTestCase):
+     def test_init(self):
+@@ -82,6 +84,19 @@ class GeometryFieldTest(SimpleTestCase):
+         with self.assertRaises(ValidationError):
+             pnt_fld.clean("LINESTRING(0 0, 1 1)")
+ 
++    def test_raster_types(self):
++        fld = forms.GeometryField()
++        for value in (
++            JSON_RASTER,
++            str(JSON_RASTER),
++            "/vsicurl/http://example.com/raster.tif",
++        ):
++            with (
++                self.subTest(value=value),
++                self.assertRaisesMessage(ValidationError, "Invalid geometry value."),
++            ):
++                fld.clean(value)
++
+     def test_to_python(self):
+         """
+         to_python() either returns a correct GEOSGeometry object or
+-- 
+2.44.4
diff --git a/meta-python/recipes-devtools/python/python3-django_5.0.14.bb b/meta-python/recipes-devtools/python/python3-django_5.0.14.bb
index 8a7cd2be16..c54e96fd7a 100644
--- a/meta-python/recipes-devtools/python/python3-django_5.0.14.bb
+++ b/meta-python/recipes-devtools/python/python3-django_5.0.14.bb
@@ -9,6 +9,7 @@  SRC_URI += "file://CVE-2025-64460.patch \
             file://CVE-2025-64459-2.patch \
             file://CVE-2025-57833.patch \
             file://CVE-2025-59681.patch \
+            file://CVE-2026-15307.patch \
            "
 SRC_URI[sha256sum] = "29019a5763dbd48da1720d687c3522ef40d1c61be6fb2fad27ed79e9f655bc11"