fix(security): handle macOS symlinked system dirs
Follow-up on PR #353 to keep dangerous-path blocking correct on macOS (/etc -> /private/etc) while avoiding overblocking Windows workspaces (C:\).
This commit is contained in:
@@ -6,10 +6,18 @@ import asyncio
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# On macOS, the default pytest temp dir is typically under /var (e.g. /private/var/folders/...).
|
||||
# If /var is considered a dangerous system path, tests must use a safe temp root (like /tmp).
|
||||
if sys.platform == "darwin":
|
||||
os.environ["TMPDIR"] = "/tmp"
|
||||
# tempfile caches the temp dir after first lookup; clear it so pytest fixtures pick up TMPDIR.
|
||||
tempfile.tempdir = None
|
||||
|
||||
# Ensure the parent directory is in the Python path for imports
|
||||
parent_dir = Path(__file__).resolve().parent.parent
|
||||
if str(parent_dir) not in sys.path:
|
||||
|
||||
@@ -16,7 +16,6 @@ DANGEROUS_SYSTEM_PATHS = {
|
||||
"/bin",
|
||||
"/var",
|
||||
"/root",
|
||||
"C:\\",
|
||||
"C:\\Windows",
|
||||
"C:\\Program Files",
|
||||
}
|
||||
@@ -122,6 +121,17 @@ def is_dangerous_path(path: Path) -> bool:
|
||||
try:
|
||||
resolved = path.resolve()
|
||||
|
||||
def _dangerous_variants(p: Path) -> set[Path]:
|
||||
variants = {p}
|
||||
# Only resolve paths that are absolute on the current platform.
|
||||
# This avoids turning Windows-style strings into nonsense absolute paths on POSIX.
|
||||
if p.is_absolute():
|
||||
try:
|
||||
variants.add(p.resolve())
|
||||
except Exception:
|
||||
pass
|
||||
return variants
|
||||
|
||||
# Check 1: Root directory (filesystem root)
|
||||
if resolved.parent == resolved:
|
||||
return True
|
||||
@@ -132,19 +142,20 @@ def is_dangerous_path(path: Path) -> bool:
|
||||
if dangerous == "/":
|
||||
continue
|
||||
|
||||
dangerous_path = Path(dangerous)
|
||||
# is_relative_to() correctly handles both exact matches and subdirectories
|
||||
# Works properly on Windows with paths like "C:\" and "C:\Windows"
|
||||
if resolved == dangerous_path or resolved.is_relative_to(dangerous_path):
|
||||
return True
|
||||
for dangerous_path in _dangerous_variants(Path(dangerous)):
|
||||
# is_relative_to() correctly handles both exact matches and subdirectories.
|
||||
# Resolving the dangerous base path also handles platform symlinks
|
||||
# (e.g., macOS /etc -> /private/etc, /var -> /private/var).
|
||||
if resolved == dangerous_path or resolved.is_relative_to(dangerous_path):
|
||||
return True
|
||||
|
||||
# Check 3: Home containers - block ONLY exact match
|
||||
# Subdirectories like /home/user/project should pass through here
|
||||
# and be handled by is_home_directory_root() in resolve_and_validate_path()
|
||||
for container in DANGEROUS_HOME_CONTAINERS:
|
||||
container_path = Path(container)
|
||||
if resolved == container_path:
|
||||
return True
|
||||
for container_path in _dangerous_variants(Path(container)):
|
||||
if resolved == container_path:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user