From 102b2fbab013b24e6ee98179dffd9fa1a6ce7057 Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Sun, 14 Jun 2026 14:32:25 +0530 Subject: [PATCH 1/2] restrict cache storage directory permissions to owner --- fsspec/implementations/cached.py | 6 +++--- fsspec/implementations/tests/test_cached.py | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/fsspec/implementations/cached.py b/fsspec/implementations/cached.py index c140f8d61..338b6407e 100644 --- a/fsspec/implementations/cached.py +++ b/fsspec/implementations/cached.py @@ -137,7 +137,7 @@ def __init__( storage = [cache_storage] else: storage = cache_storage - os.makedirs(storage[-1], exist_ok=True) + os.makedirs(storage[-1], exist_ok=True, mode=0o700) self.storage = storage self.kwargs = target_options or {} self.cache_check = cache_check @@ -185,7 +185,7 @@ def _remove_tempdir(tempdir): pass def _mkcache(self): - os.makedirs(self.storage[-1], exist_ok=True) + os.makedirs(self.storage[-1], exist_ok=True, mode=0o700) def cache_size(self): """Return size of cache in bytes. @@ -815,7 +815,7 @@ def __init__(self, **kwargs): super().__init__(**kw) for storage in self.storage: if not os.path.exists(storage): - os.makedirs(storage, exist_ok=True) + os.makedirs(storage, exist_ok=True, mode=0o700) def _check_file(self, path): self._check_cache() diff --git a/fsspec/implementations/tests/test_cached.py b/fsspec/implementations/tests/test_cached.py index 3a4727902..0c7892446 100644 --- a/fsspec/implementations/tests/test_cached.py +++ b/fsspec/implementations/tests/test_cached.py @@ -172,6 +172,24 @@ def test_constructor_kwargs(tmpdir): ) +@pytest.mark.skipif(win, reason="POSIX file permissions") +@pytest.mark.parametrize("protocol", ["filecache", "simplecache", "blockcache"]) +def test_cache_storage_not_world_readable(tmp_path, protocol): + import stat + + cache = tmp_path / "cache" # does not exist yet, fsspec must create it + old = os.umask(0o022) + try: + fsspec.filesystem( + protocol, target_protocol="file", cache_storage=str(cache) + ) + finally: + os.umask(old) + + mode = stat.S_IMODE(os.stat(cache).st_mode) + assert mode & (stat.S_IRWXG | stat.S_IRWXO) == 0, oct(mode) + + def test_idempotent(): import pickle From ef7925666f1b76e382d175bfca45dde84edf2f17 Mon Sep 17 00:00:00 2001 From: sahvx655-wq Date: Mon, 22 Jun 2026 19:35:19 +0530 Subject: [PATCH 2/2] Make cache directory mode an opt-in cache_storage_mode argument Default of None keeps the system default (umask), preserving current behaviour; passing an octal mode such as 0o700 restricts the cache storage directory to the owner. --- fsspec/implementations/cached.py | 22 ++++++++++++++++++--- fsspec/implementations/tests/test_cached.py | 7 +++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/fsspec/implementations/cached.py b/fsspec/implementations/cached.py index 338b6407e..08b4ad579 100644 --- a/fsspec/implementations/cached.py +++ b/fsspec/implementations/cached.py @@ -76,6 +76,7 @@ def __init__( same_names: bool | None = None, compression=None, cache_mapper: AbstractCacheMapper | None = None, + cache_storage_mode=None, **kwargs, ): """ @@ -118,6 +119,13 @@ def __init__( cache_mapper: AbstractCacheMapper (optional) The object use to map from original filenames to cached filenames. Only one of this and ``same_names`` should be specified. + cache_storage_mode: int (optional) + Permission mode used when creating the cache storage directory, + e.g. ``0o700``. The default of ``None`` leaves the directory at + the system default (governed by the umask), which is the existing + behaviour. Pass an octal mode such as ``0o700`` to keep the cache + directory, and so the cached data files within it, readable by the + owner only. """ super().__init__(**kwargs) if fs is None and target_protocol is None: @@ -137,7 +145,8 @@ def __init__( storage = [cache_storage] else: storage = cache_storage - os.makedirs(storage[-1], exist_ok=True, mode=0o700) + self.cache_storage_mode = cache_storage_mode + self._makedirs(storage[-1]) self.storage = storage self.kwargs = target_options or {} self.cache_check = cache_check @@ -184,8 +193,14 @@ def _remove_tempdir(tempdir): except Exception: pass + def _makedirs(self, path): + if self.cache_storage_mode is None: + os.makedirs(path, exist_ok=True) + else: + os.makedirs(path, exist_ok=True, mode=self.cache_storage_mode) + def _mkcache(self): - os.makedirs(self.storage[-1], exist_ok=True, mode=0o700) + self._makedirs(self.storage[-1]) def cache_size(self): """Return size of cache in bytes. @@ -452,6 +467,7 @@ def __getattribute__(self, item): "isdir", "_check_file", "_check_cache", + "_makedirs", "_mkcache", "clear_cache", "clear_expired_cache", @@ -815,7 +831,7 @@ def __init__(self, **kwargs): super().__init__(**kw) for storage in self.storage: if not os.path.exists(storage): - os.makedirs(storage, exist_ok=True, mode=0o700) + self._makedirs(storage) def _check_file(self, path): self._check_cache() diff --git a/fsspec/implementations/tests/test_cached.py b/fsspec/implementations/tests/test_cached.py index 0c7892446..794eea061 100644 --- a/fsspec/implementations/tests/test_cached.py +++ b/fsspec/implementations/tests/test_cached.py @@ -174,14 +174,17 @@ def test_constructor_kwargs(tmpdir): @pytest.mark.skipif(win, reason="POSIX file permissions") @pytest.mark.parametrize("protocol", ["filecache", "simplecache", "blockcache"]) -def test_cache_storage_not_world_readable(tmp_path, protocol): +def test_cache_storage_mode(tmp_path, protocol): import stat cache = tmp_path / "cache" # does not exist yet, fsspec must create it old = os.umask(0o022) try: fsspec.filesystem( - protocol, target_protocol="file", cache_storage=str(cache) + protocol, + target_protocol="file", + cache_storage=str(cache), + cache_storage_mode=0o700, ) finally: os.umask(old)