Opened 106 minutes ago
Last modified 100 minutes ago
#37289 assigned Bug
GDALRaster.__del__() shouldn't delete rasters opened in read-only mode — at Initial Version
| 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
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 back to True. 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): 217 217 ) 218 218 219 219 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 ): 221 224 # Remove the temporary file from the VSI in-memory filesystem. 222 225 capi.unlink_vsi_file(force_bytes(self.name)) 223 226 super().__del__() … … class GDALRaster(GDALRasterBase): 276 279 277 280 @property 278 281 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: 282 283 return None 283 284 # Prepare an integer that will contain the buffer length. 284 285 out_length = c_int() … … class GDALRaster(GDALRasterBase): 295 296 def is_vsi_based(self): 296 297 return self._ptr and self.name.startswith(VSI_FILESYSTEM_PREFIX) 297 298 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 298 303 @property 299 304 def name(self): 300 305 """ -
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): 283 283 self.assertEqual(rst.name, rst_path) 284 284 self.assertIs(rst.is_vsi_based, True) 285 285 self.assertIsNone(rst.vsi_buffer) 286 # assert the virtual file is still there, needs release note... 287 # retry with GDALRaster(..., write=False) 288 # assert the virtual file is gone... 286 289 287 290 def test_offset_size_and_shape_on_raster_creation(self): 288 291 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.