Skip to content

Commit 5b72667

Browse files
add verification of directory permissions too
1 parent ab472d4 commit 5b72667

File tree

2 files changed

+96
-12
lines changed

2 files changed

+96
-12
lines changed

src/snowflake/connector/crl_cache.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,19 @@ def __init__(
227227
self._ensure_cache_directory_exists()
228228

229229
def _ensure_cache_directory_exists(self) -> None:
230-
"""Create the cache directory if it doesn't exist."""
230+
"""Create the cache directory if it doesn't exist with secure permissions."""
231231
try:
232-
self._cache_dir.mkdir(parents=True, exist_ok=True)
232+
# Create directory with secure permissions (owner read/write/execute only)
233+
self._cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
234+
235+
# Verify directory permissions (if it already existed)
236+
if not self._unsafe_skip_file_permissions_check:
237+
self._check_permissions(self._cache_dir, "directory", "0o700")
238+
233239
logger.debug(f"Cache directory created/verified: {self._cache_dir}")
240+
except PermissionError:
241+
# Re-raise permission errors as-is
242+
raise
234243
except OSError as e:
235244
raise OSError(f"Failed to create cache directory {self._cache_dir}: {e}")
236245

@@ -256,38 +265,42 @@ def _get_crl_file_lock(self, crl_cache_file: Path) -> BaseFileLock:
256265
timeout=self._cache_file_lock_timeout,
257266
)
258267

259-
def _check_file_permissions(self, file_path: Path) -> None:
268+
def _check_permissions(
269+
self, path: Path, resource_type: str, expected_perms: str
270+
) -> None:
260271
"""
261-
Check that the CRL cache file has secure permissions (owner-only access).
272+
Check that a CRL cache resource has secure permissions (owner-only access).
262273
263274
Note: This check is only performed on Unix-like systems. Windows file
264275
permissions work differently and are not checked.
265276
266277
Args:
267-
file_path: Path to the file to check
278+
path: Path to the resource (file or directory) to check
279+
resource_type: Description of the resource type (e.g., "file", "directory")
280+
expected_perms: Description of expected permissions (e.g., "0o600 or 0o400", "0o700")
268281
269282
Raises:
270-
PermissionError: If file permissions are too wide or file has wrong owner
283+
PermissionError: If resource permissions are too wide
271284
"""
272285
# Skip permission checks on Windows as they work differently
273286
if IS_WINDOWS:
274287
return
275288

276289
try:
277-
stat_info = file_path.stat()
290+
stat_info = path.stat()
278291
actual_permissions = stat.S_IMODE(stat_info.st_mode)
279292

280-
# Check that file is readable/writable only by owner (0o600 or more restrictive)
293+
# Check that resource is accessible only by owner (no group/other permissions)
281294
if (
282295
actual_permissions & 0o077 != 0
283296
): # Check if group or others have any permission
284297
raise PermissionError(
285-
f"CRL cache file {file_path} has insecure permissions: {oct(actual_permissions)}. "
286-
f"File must be accessible only by the owner (e.g., 0o600 or 0o400)."
298+
f"CRL cache {resource_type} {path} has insecure permissions: {oct(actual_permissions)}. "
299+
f"{resource_type.capitalize()} must be accessible only by the owner ({expected_perms})."
287300
)
288301

289302
except FileNotFoundError:
290-
# File doesn't exist yet, this is fine
303+
# Resource doesn't exist yet, this is fine
291304
pass
292305

293306
def get(self, crl_url: str) -> CRLCacheEntry | None:
@@ -308,7 +321,7 @@ def get(self, crl_url: str) -> CRLCacheEntry | None:
308321

309322
# Check file permissions before reading
310323
if not self._unsafe_skip_file_permissions_check:
311-
self._check_file_permissions(crl_file_path)
324+
self._check_permissions(crl_file_path, "file", "0o600 or 0o400")
312325
else:
313326
logger.warning(
314327
f"Skipping file permissions check for {crl_file_path}"

test/unit/test_crl_cache.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,3 +707,74 @@ def test_file_cache_permission_validation(
707707
# Secure permissions or checks disabled - should read successfully
708708
assert result is not None
709709
assert result.crl is not None
710+
711+
712+
@pytest.mark.skipif(
713+
IS_WINDOWS, reason="File permission checks not applicable on Windows"
714+
)
715+
def test_cache_directory_created_with_secure_permissions():
716+
"""Test that cache directory is created with owner-only permissions (0o700)"""
717+
with tempfile.TemporaryDirectory() as temp_dir:
718+
cache_dir = Path(temp_dir) / "crl_cache"
719+
# initialize the cache directory
720+
CRLFileCache(cache_dir, timedelta(hours=1))
721+
722+
# Check directory permissions
723+
dir_stat = cache_dir.stat()
724+
permissions = stat.S_IMODE(dir_stat.st_mode)
725+
726+
# Should be 0o700 (read/write/execute for owner only)
727+
assert permissions == 0o700, f"Expected 0o700, got {oct(permissions)}"
728+
729+
730+
@pytest.mark.skipif(
731+
IS_WINDOWS, reason="File permission checks not applicable on Windows"
732+
)
733+
@pytest.mark.parametrize(
734+
"chmod_permissions,skip_check,should_raise",
735+
[
736+
(0o755, False, True), # rwxr-xr-x (world-readable) with checks - should raise
737+
(0o750, False, True), # rwxr-x--- (group-readable) with checks - should raise
738+
(0o700, False, False), # rwx------ (owner only) with checks - should not raise
739+
(
740+
0o755,
741+
True,
742+
False,
743+
), # rwxr-xr-x (world-readable) without checks - should not raise
744+
],
745+
ids=[
746+
"world-readable-dir",
747+
"group-readable-dir",
748+
"owner-only-dir",
749+
"skip-check-dir",
750+
],
751+
)
752+
def test_cache_directory_permission_validation(
753+
chmod_permissions, skip_check, should_raise
754+
):
755+
"""Test that cache directory permissions are validated correctly"""
756+
with tempfile.TemporaryDirectory() as temp_dir:
757+
cache_dir = Path(temp_dir) / "crl_cache"
758+
cache_dir.mkdir(mode=0o700)
759+
760+
# Change directory permissions before creating cache
761+
os.chmod(cache_dir, chmod_permissions)
762+
763+
if should_raise:
764+
# Should raise PermissionError for insecure directory
765+
with pytest.raises(
766+
PermissionError, match="cache directory.*insecure permissions"
767+
):
768+
CRLFileCache(
769+
cache_dir,
770+
timedelta(hours=1),
771+
unsafe_skip_file_permissions_check=skip_check,
772+
)
773+
else:
774+
# Should succeed with secure permissions or when checks are disabled
775+
cache = CRLFileCache(
776+
cache_dir,
777+
timedelta(hours=1),
778+
unsafe_skip_file_permissions_check=skip_check,
779+
)
780+
assert cache is not None

0 commit comments

Comments
 (0)