Compare commits

..

6 Commits

Author SHA1 Message Date
cfaf6c827e security: validate CDP URLs to prevent SSRF (V-010, CVSS 8.4)
Some checks failed
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 27s
Tests / test (pull_request) Failing after 25s
Docker Build and Publish / build-and-push (pull_request) Failing after 37s
Add URL validation before fetching Chrome DevTools Protocol endpoints.
Only allows localhost and private network addresses.

Changes:
- tools/browser_tool.py: Add hostname validation in _resolve_cdp_override()
- Block external URLs to prevent SSRF attacks
- Log security errors for rejected URLs

CVSS: 8.4 (High)
Refs: V-010 in SECURITY_AUDIT_REPORT.md
CWE-918: Server-Side Request Forgery
2026-03-30 23:57:22 +00:00
cf1afb07f2 Merge pull request '[SECURITY] Block Dangerous Docker Volume Mounts (V-012, CVSS 8.7)' (#64) from security/fix-docker-privilege into main
Some checks failed
Nix / nix (ubuntu-latest) (push) Failing after 12s
Tests / test (push) Failing after 18s
Docker Build and Publish / build-and-push (push) Failing after 45s
Nix / nix (macos-latest) (push) Has been cancelled
2026-03-30 23:55:50 +00:00
ed32487cbe security: block dangerous Docker volume mounts (V-012, CVSS 8.7)
Some checks failed
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 28s
Tests / test (pull_request) Failing after 29s
Docker Build and Publish / build-and-push (pull_request) Failing after 42s
Prevent privilege escalation via Docker socket mount.

Changes:
- tools/environments/docker.py: Add _is_dangerous_volume() validation
- Block docker.sock, /proc, /sys, /dev, root fs mounts
- Log security error when dangerous volume detected

Fixes container escape vulnerability where user-configured volumes
could mount Docker socket for host compromise.

CVSS: 8.7 (High)
Refs: V-012 in SECURITY_AUDIT_REPORT.md
CWE-250: Execution with Unnecessary Privileges
2026-03-30 23:55:45 +00:00
37c5e672b5 Merge pull request '[SECURITY] Fix Auth Bypass & CORS Misconfiguration (V-008, V-009)' (#63) from security/fix-auth-bypass into main
Some checks failed
Nix / nix (ubuntu-latest) (push) Failing after 6s
Docker Build and Publish / build-and-push (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-03-30 23:55:04 +00:00
cfcffd38ab security: fix auth bypass and CORS misconfiguration (V-008, V-009)
Some checks failed
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Successful in 27s
Tests / test (pull_request) Failing after 24s
Docker Build and Publish / build-and-push (pull_request) Failing after 35s
API Server security hardening:

V-009 (CVSS 8.1) - Authentication Bypass Fix:
- Changed default from allow-all to deny-all when no API key configured
- Added explicit API_SERVER_ALLOW_UNAUTHENTICATED setting for local dev
- Added warning logs for both secure and insecure configurations

V-008 (CVSS 8.2) - CORS Misconfiguration Fix:
- Reject wildcard '*' CORS origins (security vulnerability with credentials)
- Require explicit origin configuration
- Added warning log when wildcard detected

Changes:
- gateway/platforms/api_server.py: Hardened auth and CORS handling

Refs: V-008, V-009 in SECURITY_AUDIT_REPORT.md
CWE-287: Improper Authentication
CWE-942: Permissive Cross-domain Policy
2026-03-30 23:54:58 +00:00
0b49540db3 Merge pull request '[FIX] Cross-Process Locking for SQLite Contention (Issue #52)' (#62) from fix/sqlite-contention into main
Some checks failed
Nix / nix (ubuntu-latest) (push) Failing after 5s
Tests / test (push) Failing after 15s
Docker Build and Publish / build-and-push (push) Failing after 44s
Nix / nix (macos-latest) (push) Has been cancelled
2026-03-30 23:51:05 +00:00
3 changed files with 110 additions and 8 deletions

View File

@@ -292,7 +292,29 @@ class APIServerAdapter(BasePlatformAdapter):
extra = config.extra or {}
self._host: str = extra.get("host", os.getenv("API_SERVER_HOST", DEFAULT_HOST))
self._port: int = int(extra.get("port", os.getenv("API_SERVER_PORT", str(DEFAULT_PORT))))
# SECURITY FIX (V-009): Fail-secure default for API key
# Previously: Empty API key allowed all requests (dangerous default)
# Now: Require explicit "allow_unauthenticated" setting to disable auth
self._api_key: str = extra.get("key", os.getenv("API_SERVER_KEY", ""))
self._allow_unauthenticated: bool = extra.get(
"allow_unauthenticated",
os.getenv("API_SERVER_ALLOW_UNAUTHENTICATED", "").lower() in ("true", "1", "yes")
)
# SECURITY: Log warning if no API key configured
if not self._api_key and not self._allow_unauthenticated:
logger.warning(
"API_SERVER_KEY not configured. All requests will be rejected. "
"Set API_SERVER_ALLOW_UNAUTHENTICATED=true for local-only use, "
"or configure API_SERVER_KEY for production."
)
elif not self._api_key and self._allow_unauthenticated:
logger.warning(
"API_SERVER running without authentication. "
"This is only safe for local-only deployments."
)
self._cors_origins: tuple[str, ...] = self._parse_cors_origins(
extra.get("cors_origins", os.getenv("API_SERVER_CORS_ORIGINS", "")),
)
@@ -317,15 +339,22 @@ class APIServerAdapter(BasePlatformAdapter):
return tuple(str(item).strip() for item in items if str(item).strip())
def _cors_headers_for_origin(self, origin: str) -> Optional[Dict[str, str]]:
"""Return CORS headers for an allowed browser origin."""
"""Return CORS headers for an allowed browser origin.
SECURITY FIX (V-008): Never allow wildcard "*" with credentials.
If "*" is configured, we reject the request to prevent security issues.
"""
if not origin or not self._cors_origins:
return None
# SECURITY FIX (V-008): Reject wildcard CORS origins
# Wildcard with credentials is a security vulnerability
if "*" in self._cors_origins:
headers = dict(_CORS_HEADERS)
headers["Access-Control-Allow-Origin"] = "*"
headers["Access-Control-Max-Age"] = "600"
return headers
logger.warning(
"CORS wildcard '*' is not allowed for security reasons. "
"Please configure specific origins in API_SERVER_CORS_ORIGINS."
)
return None # Reject wildcard - too dangerous
if origin not in self._cors_origins:
return None
@@ -355,10 +384,22 @@ class APIServerAdapter(BasePlatformAdapter):
Validate Bearer token from Authorization header.
Returns None if auth is OK, or a 401 web.Response on failure.
If no API key is configured, all requests are allowed.
SECURITY FIX (V-009): Fail-secure default
- If no API key is configured AND allow_unauthenticated is not set,
all requests are rejected (secure by default)
- Only allow unauthenticated requests if explicitly configured
"""
if not self._api_key:
return None # No key configured — allow all (local-only use)
# SECURITY: Fail-secure default - reject if no key and not explicitly allowed
if not self._api_key and not self._allow_unauthenticated:
return web.json_response(
{"error": {"message": "Authentication required. Configure API_SERVER_KEY or set API_SERVER_ALLOW_UNAUTHENTICATED=true for local development.", "type": "authentication_error", "code": "auth_required"}},
status=401,
)
# Allow unauthenticated requests only if explicitly configured
if not self._api_key and self._allow_unauthenticated:
return None # Explicitly allowed for local-only use
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):

View File

@@ -170,6 +170,9 @@ def _resolve_cdp_override(cdp_url: str) -> str:
For discovery-style endpoints we fetch /json/version and return the
webSocketDebuggerUrl so downstream tools always receive a concrete browser
websocket instead of an ambiguous host:port URL.
SECURITY FIX (V-010): Validates URLs before fetching to prevent SSRF.
Only allows localhost/private network addresses for CDP connections.
"""
raw = (cdp_url or "").strip()
if not raw:
@@ -191,6 +194,35 @@ def _resolve_cdp_override(cdp_url: str) -> str:
else:
version_url = discovery_url.rstrip("/") + "/json/version"
# SECURITY FIX (V-010): Validate URL before fetching
# Only allow localhost and private networks for CDP
from urllib.parse import urlparse
parsed = urlparse(version_url)
hostname = parsed.hostname or ""
# Allow only safe hostnames for CDP
allowed_hostnames = ["localhost", "127.0.0.1", "0.0.0.0", "::1"]
if hostname not in allowed_hostnames:
# Check if it's a private IP
try:
import ipaddress
ip = ipaddress.ip_address(hostname)
if not (ip.is_private or ip.is_loopback):
logger.error(
"SECURITY: Rejecting CDP URL '%s' - only localhost and private "
"networks are allowed to prevent SSRF attacks.",
raw
)
return raw # Return original without fetching
except ValueError:
# Not an IP - reject unknown hostnames
logger.error(
"SECURITY: Rejecting CDP URL '%s' - unknown hostname '%s'. "
"Only localhost and private IPs are allowed.",
raw, hostname
)
return raw
try:
response = requests.get(version_url, timeout=10)
response.raise_for_status()

View File

@@ -253,6 +253,26 @@ class DockerEnvironment(BaseEnvironment):
# mode uses tmpfs (ephemeral, fast, gone on cleanup).
from tools.environments.base import get_sandbox_dir
# SECURITY FIX (V-012): Block dangerous volume mounts
# Prevent privilege escalation via Docker socket or sensitive paths
_BLOCKED_VOLUME_PATTERNS = [
"/var/run/docker.sock",
"/run/docker.sock",
"/var/run/docker.pid",
"/proc", "/sys", "/dev",
":/", # Root filesystem mount
]
def _is_dangerous_volume(vol_spec: str) -> bool:
"""Check if volume spec is dangerous (docker socket, root fs, etc)."""
for pattern in _BLOCKED_VOLUME_PATTERNS:
if pattern in vol_spec:
return True
# Check for docker socket variations
if "docker.sock" in vol_spec.lower():
return True
return False
# User-configured volume mounts (from config.yaml docker_volumes)
volume_args = []
workspace_explicitly_mounted = False
@@ -263,6 +283,15 @@ class DockerEnvironment(BaseEnvironment):
vol = vol.strip()
if not vol:
continue
# SECURITY FIX (V-012): Block dangerous volumes
if _is_dangerous_volume(vol):
logger.error(
f"SECURITY: Refusing to mount dangerous volume '{vol}'. "
f"Docker socket and system paths are blocked to prevent container escape."
)
continue # Skip this dangerous volume
if ":" in vol:
volume_args.extend(["-v", vol])
if ":/workspace" in vol: