Opened 106 minutes ago

Last modified 100 minutes ago

#37289 assigned Bug

GDALRaster.__del__() shouldn't delete rasters opened in read-only mode — at Version 3

Reported by: Jacob Walls Owned by: Django Sprints
Component: GIS Version: 5.2
Severity: Release blocker Keywords: not-security, gdal
Cc: Jordi Castells, Daniel Wiesmann Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Jacob Walls)

GDALRaster.__del__(), implemented in #28300, deletes the raster file that backs the python object if it's located on GDAL's virtual filesystem, because at the time, the only virtual filesystem in use was the in-memory one, and the principal (only?) use was for temporary files, as you can see from this comment:

    def __del__(self):
        if self.is_vsi_based:
            # Remove the temporary file from the VSI in-memory filesystem.
            capi.unlink_vsi_file(force_bytes(self.name))
        super().__del__()

... and this constant:

# Should the memory file system take ownership of the buffer, freeing it when
# the file is deleted? (No, GDALRaster.__del__() will delete the buffer.)
VSI_TAKE_BUFFER_OWNERSHIP = False

Later, by adding support for all virtual fileystems, #32670 added support for compressed and network rasters, see docs.

Since GDALRaster.__del__() was not adjusted, a security report observed that when the python object dies, a delete is fired off and could delete a network raster from a commercial storage provider like S3 (using the vsis3 driver).

The Security Team rejected the report on the basis that even assuming some other legitimate use for assigning delete permissions to the web process user, e.g. in some other workflow, since this deletion happens on every legitimate use as well, this would be discovered immediately in even the most preliminary user acceptance testing, and the feature would be withdrawn, worked around, or reconfigured with reduced permissions well before an attacker could reach it. (But potentially after a few unintentionally deleted rasters, hence marking as a data loss bug / 5.2 release blocker.)

Thanks "garden_" for the informative report.


GDALRaster defaults the write parameter to False. Paths are one of the input classes where that parameter is respected instead of overwritten later via self._write = 1. As an implementation hint, I would expect something like:

  • django/contrib/gis/gdal/raster/source.py

    diff --git a/django/contrib/gis/gdal/raster/source.py b/django/contrib/gis/gdal/raster/source.py
    index 42ab1e3a70..d3a6e7deee 100644
    a b class GDALRaster(GDALRasterBase):  
    217217            )
    218218
    219219    def __del__(self):
    220         if self.is_vsi_based:
     220        if (
     221            (self.is_vsi_based and self._write)
     222            or self.is_vsi_in_memory_based
     223        ):
    221224            # Remove the temporary file from the VSI in-memory filesystem.
    222225            capi.unlink_vsi_file(force_bytes(self.name))
    223226        super().__del__()
    class GDALRaster(GDALRasterBase):  
    276279
    277280    @property
    278281    def vsi_buffer(self):
    279         if not (
    280             self.is_vsi_based and self.name.startswith(VSI_MEM_FILESYSTEM_BASE_PATH)
    281         ):
     282        if not self.is_vsi_in_memory_based:
    282283            return None
    283284        # Prepare an integer that will contain the buffer length.
    284285        out_length = c_int()
    class GDALRaster(GDALRasterBase):  
    295296    def is_vsi_based(self):
    296297        return self._ptr and self.name.startswith(VSI_FILESYSTEM_PREFIX)
    297298
     299    @cached_property
     300    def is_vsi_in_memory_based(self):  # help with naming, please
     301        return self._ptr and self.name.startswith(VSI_MEM_FILESYSTEM_BASE_PATH)
     302
    298303    @property
    299304    def name(self):
    300305        """
  • tests/gis_tests/gdal_tests/test_raster.py

    diff --git a/tests/gis_tests/gdal_tests/test_raster.py b/tests/gis_tests/gdal_tests/test_raster.py
    index ca7251914b..7c21a9c343 100644
    a b class GDALRasterTests(SimpleTestCase):  
    283283        self.assertEqual(rst.name, rst_path)
    284284        self.assertIs(rst.is_vsi_based, True)
    285285        self.assertIsNone(rst.vsi_buffer)
     286        # assert the virtual file is still there, needs release note...
     287        # retry with GDALRaster(..., write=True)
     288        # assert the virtual file is gone...
    286289
    287290    def test_offset_size_and_shape_on_raster_creation(self):
    288291        rast = GDALRaster(

... but that will need a release note as in the compressed raster case (e.g. /vsizip/..., this will require passing write=True to "take ownership" of the delete). CCing other knowledgable folks for opinions.

Change History (3)

comment:1 by Sarah Boyce, 104 minutes ago

Triage Stage: UnreviewedAccepted

comment:2 by Jacob Walls, 103 minutes ago

Description: modified (diff)

comment:3 by Jacob Walls, 100 minutes ago

Description: modified (diff)
Note: See TracTickets for help on using tickets.
Back to Top