mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
security: harden EXTRA_READ_PATHS with validation and blocklist
Add security controls to the EXTRA_READ_PATHS feature (PR #126) to prevent path traversal attacks and accidental exposure of sensitive directories. Changes: - Add EXTRA_READ_PATHS_BLOCKLIST constant blocking credential directories (.ssh, .aws, .azure, .kube, .gnupg, .docker, .npmrc, .pypirc, .netrc) - Create get_extra_read_paths() function with comprehensive validation: - Path canonicalization via Path.resolve() to prevent .. traversal - Validates paths are absolute (rejects relative paths) - Validates paths exist and are directories - Blocks paths that are/contain sensitive directories - Blocks paths that would expose sensitive dirs (e.g., home dir) - Update create_client() to use validated getter function - Improve logging to show validated paths instead of raw input - Document security controls in CLAUDE.md under Security Model section Security considerations: - Addresses path traversal risk similar to CVE-2025-54794 - Prevents accidental exposure of SSH keys, cloud credentials, etc. - All validation happens before permissions are granted to the agent Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
40
CLAUDE.md
40
CLAUDE.md
@@ -211,6 +211,46 @@ Defense-in-depth approach configured in `client.py`:
|
||||
2. Filesystem restricted to project directory only
|
||||
3. Bash commands validated using hierarchical allowlist system
|
||||
|
||||
#### Extra Read Paths (Cross-Project File Access)
|
||||
|
||||
The agent can optionally read files from directories outside the project folder via the `EXTRA_READ_PATHS` environment variable. This enables referencing documentation, shared libraries, or other projects.
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
# Single path
|
||||
EXTRA_READ_PATHS=/Users/me/docs
|
||||
|
||||
# Multiple paths (comma-separated)
|
||||
EXTRA_READ_PATHS=/Users/me/docs,/opt/shared-libs,/Volumes/Data/reference
|
||||
```
|
||||
|
||||
**Security Controls:**
|
||||
|
||||
All paths are validated before being granted read access:
|
||||
- Must be absolute paths (not relative)
|
||||
- Must exist and be directories
|
||||
- Paths are canonicalized via `Path.resolve()` to prevent `..` traversal attacks
|
||||
- Sensitive directories are blocked (see blocklist below)
|
||||
- Only Read, Glob, and Grep operations are allowed (no Write/Edit)
|
||||
|
||||
**Blocked Sensitive Directories:**
|
||||
|
||||
The following directories (relative to home) are always blocked:
|
||||
- `.ssh`, `.aws`, `.azure`, `.kube` - Cloud/SSH credentials
|
||||
- `.gnupg`, `.gpg`, `.password-store` - Encryption keys
|
||||
- `.docker`, `.config/gcloud` - Container/cloud configs
|
||||
- `.npmrc`, `.pypirc`, `.netrc` - Package manager credentials
|
||||
|
||||
**Example Output:**
|
||||
|
||||
```
|
||||
Created security settings at /path/to/project/.claude_settings.json
|
||||
- Sandbox enabled (OS-level bash isolation)
|
||||
- Filesystem restricted to: /path/to/project
|
||||
- Extra read paths (validated): /Users/me/docs, /opt/shared-libs
|
||||
```
|
||||
|
||||
#### Per-Project Allowed Commands
|
||||
|
||||
The agent's bash command access is controlled through a hierarchical configuration system:
|
||||
|
||||
102
client.py
102
client.py
@@ -47,6 +47,23 @@ API_ENV_VARS = [
|
||||
# Example: EXTRA_READ_PATHS=/Volumes/Data/dev,/Users/shared/libs
|
||||
EXTRA_READ_PATHS_VAR = "EXTRA_READ_PATHS"
|
||||
|
||||
# Sensitive directories that should never be allowed via EXTRA_READ_PATHS
|
||||
# These contain credentials, keys, or system-critical files
|
||||
EXTRA_READ_PATHS_BLOCKLIST = {
|
||||
".ssh",
|
||||
".aws",
|
||||
".azure",
|
||||
".kube",
|
||||
".gnupg",
|
||||
".gpg",
|
||||
".password-store",
|
||||
".docker",
|
||||
".config/gcloud",
|
||||
".npmrc",
|
||||
".pypirc",
|
||||
".netrc",
|
||||
}
|
||||
|
||||
|
||||
def get_playwright_headless() -> bool:
|
||||
"""
|
||||
@@ -85,6 +102,79 @@ def get_playwright_browser() -> str:
|
||||
return value
|
||||
|
||||
|
||||
def get_extra_read_paths() -> list[Path]:
|
||||
"""
|
||||
Get extra read-only paths from EXTRA_READ_PATHS environment variable.
|
||||
|
||||
Parses comma-separated absolute paths and validates each one:
|
||||
- Must be an absolute path
|
||||
- Must exist and be a directory
|
||||
- Cannot be or contain sensitive directories (e.g., .ssh, .aws)
|
||||
|
||||
Returns:
|
||||
List of validated, canonicalized Path objects.
|
||||
"""
|
||||
raw_value = os.getenv(EXTRA_READ_PATHS_VAR, "").strip()
|
||||
if not raw_value:
|
||||
return []
|
||||
|
||||
validated_paths: list[Path] = []
|
||||
home_dir = Path.home()
|
||||
|
||||
for path_str in raw_value.split(","):
|
||||
path_str = path_str.strip()
|
||||
if not path_str:
|
||||
continue
|
||||
|
||||
# Parse and canonicalize the path
|
||||
try:
|
||||
path = Path(path_str).resolve()
|
||||
except (OSError, ValueError) as e:
|
||||
print(f" - Warning: Invalid EXTRA_READ_PATHS path '{path_str}': {e}")
|
||||
continue
|
||||
|
||||
# Must be absolute (resolve() makes it absolute, but check original input)
|
||||
if not Path(path_str).is_absolute():
|
||||
print(f" - Warning: EXTRA_READ_PATHS requires absolute paths, skipping: {path_str}")
|
||||
continue
|
||||
|
||||
# Must exist
|
||||
if not path.exists():
|
||||
print(f" - Warning: EXTRA_READ_PATHS path does not exist, skipping: {path_str}")
|
||||
continue
|
||||
|
||||
# Must be a directory
|
||||
if not path.is_dir():
|
||||
print(f" - Warning: EXTRA_READ_PATHS path is not a directory, skipping: {path_str}")
|
||||
continue
|
||||
|
||||
# Check against sensitive directory blocklist
|
||||
is_blocked = False
|
||||
for sensitive in EXTRA_READ_PATHS_BLOCKLIST:
|
||||
sensitive_path = (home_dir / sensitive).resolve()
|
||||
try:
|
||||
# Block if path IS the sensitive dir or is INSIDE it
|
||||
if path == sensitive_path or path.is_relative_to(sensitive_path):
|
||||
print(f" - Warning: EXTRA_READ_PATHS blocked sensitive path: {path_str}")
|
||||
is_blocked = True
|
||||
break
|
||||
# Also block if sensitive dir is INSIDE the requested path
|
||||
if sensitive_path.is_relative_to(path):
|
||||
print(f" - Warning: EXTRA_READ_PATHS path contains sensitive directory ({sensitive}): {path_str}")
|
||||
is_blocked = True
|
||||
break
|
||||
except (OSError, ValueError):
|
||||
# is_relative_to can raise on some edge cases
|
||||
continue
|
||||
|
||||
if is_blocked:
|
||||
continue
|
||||
|
||||
validated_paths.append(path)
|
||||
|
||||
return validated_paths
|
||||
|
||||
|
||||
# Feature MCP tools for feature/test management
|
||||
FEATURE_MCP_TOOLS = [
|
||||
# Core feature operations
|
||||
@@ -209,12 +299,10 @@ def create_client(
|
||||
]
|
||||
|
||||
# Add extra read paths from environment variable (read-only access)
|
||||
extra_read_paths = os.getenv(EXTRA_READ_PATHS_VAR, "")
|
||||
if extra_read_paths:
|
||||
for path in extra_read_paths.split(","):
|
||||
path = path.strip()
|
||||
if path:
|
||||
# Add read-only permissions for each extra path
|
||||
# Paths are validated, canonicalized, and checked against sensitive blocklist
|
||||
extra_read_paths = get_extra_read_paths()
|
||||
for path in extra_read_paths:
|
||||
# Add read-only permissions for each validated path
|
||||
permissions_list.append(f"Read({path}/**)")
|
||||
permissions_list.append(f"Glob({path}/**)")
|
||||
permissions_list.append(f"Grep({path}/**)")
|
||||
@@ -246,7 +334,7 @@ def create_client(
|
||||
print(" - Sandbox enabled (OS-level bash isolation)")
|
||||
print(f" - Filesystem restricted to: {project_dir.resolve()}")
|
||||
if extra_read_paths:
|
||||
print(f" - Extra read paths: {extra_read_paths}")
|
||||
print(f" - Extra read paths (validated): {', '.join(str(p) for p in extra_read_paths)}")
|
||||
print(" - Bash commands restricted to allowlist (see security.py)")
|
||||
if yolo_mode:
|
||||
print(" - MCP servers: features (database) - YOLO MODE (no Playwright)")
|
||||
|
||||
Reference in New Issue
Block a user