Compare commits

..

2 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

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()