feat: plan 16 P2 batch 1 (SEC-13 SSRF, SEC-27 log-escape, SEC-28 path-traversal)
- SEC-13 (M-09): SSRF allowlist on provider-usage-fetch (always block non-http(s) schemes; enforced mode blocks internal/link-local IPs + non-allowlisted hosts, dev keeps loopback mocks); dashboard refuses non-loopback bind in enforced mode. - SEC-27 (X-02): casan-log strips ESC/CSI + CR/LF (terminal-escape + fake-log-line injection) while keeping tab and visible text. - SEC-28 (X-04): new path-guard.sh — realpath resolve + reject symlink/.. escapes outside the allowed root. Verify: SEC-13 6/0, SEC-27 3/0, SEC-28 4/0, adversarial 44/44, run-casan4 0-FAIL. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3432ae59e1
commit
8c06a55aed
@@ -24,6 +24,52 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
||||
OUT="${2:-$PROJECT_ROOT/.specify/logs/level5/provider-usage.jsonl}"
|
||||
mkdir -p "$(dirname "$OUT")"
|
||||
|
||||
# SEC-13 (M-09): SSRF guard on the fetch URL. ALWAYS reject non-http(s) schemes
|
||||
# (file://, gopher://, dict://, … metadata exfil). In enforced mode additionally
|
||||
# require the host to be in the provider allowlist and block internal/link-local
|
||||
# IPs — dev keeps loopback mocks working (http://127.0.0.1 test servers).
|
||||
if ! python3 - "$API_URL" <<'PY'
|
||||
import ipaddress, os, sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
url = sys.argv[1]
|
||||
u = urlparse(url)
|
||||
scheme = (u.scheme or "").lower()
|
||||
host = (u.hostname or "").lower()
|
||||
enforced = os.environ.get("CASAN_PROFILE") == "prod" or os.environ.get("CASAN_SSRF_STRICT") == "1"
|
||||
allow = [h.strip().lower() for h in os.environ.get(
|
||||
"CASAN_PROVIDER_HOST_ALLOWLIST", "api.openai.com,api.anthropic.com").split(",") if h.strip()]
|
||||
|
||||
def die(reason):
|
||||
sys.stderr.write(f"PROVIDER_URL_REJECTED {reason} url={url}\n")
|
||||
sys.exit(1)
|
||||
|
||||
if scheme not in ("http", "https"):
|
||||
die(f"scheme_not_allowed:{scheme or 'none'}") # blocks file:// et al (all modes)
|
||||
if not host:
|
||||
die("no_host")
|
||||
|
||||
if enforced:
|
||||
if scheme != "https":
|
||||
die("plaintext_http_not_allowed_in_prod")
|
||||
if host in ("localhost",) or host.endswith(".internal") or host.endswith(".local"):
|
||||
die(f"internal_host:{host}")
|
||||
try:
|
||||
ip = ipaddress.ip_address(host)
|
||||
if (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved
|
||||
or ip.is_multicast or ip.is_unspecified):
|
||||
die(f"internal_ip:{host}")
|
||||
except ValueError:
|
||||
pass # a hostname, not a literal IP
|
||||
if allow and host not in allow:
|
||||
die(f"host_not_in_allowlist:{host}")
|
||||
sys.exit(0)
|
||||
PY
|
||||
then
|
||||
echo "PROVIDER_API_SSRF_BLOCKED url=$API_URL (telemetry NOT imported)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BODY="$(mktemp)"
|
||||
trap 'rm -f "$BODY"' EXIT
|
||||
|
||||
|
||||
Reference in New Issue
Block a user