security: fix audit findings (1 critical, 7 high, med/low) - #1154
security: fix audit findings (1 critical, 7 high, med/low)#1154omranalamri wants to merge 3 commits into
Conversation
Static audit of the full tree surfaced containment gaps in an otherwise sound design (no docker.sock, no privileged containers, LLM keys never enter the sandbox, SDK tracing hard-disabled). Fixes, by area: Sandbox / container (containers/, runtime/): - CRITICAL: MITM root CA private key was generated at image BUILD time, so every user of the published image shared one key that signs all intercepted TLS. Moved CA generation to per-container runtime in the entrypoint; the .p12 now uses a random per-run password (0600), not "". - Caido proxy bound 0.0.0.0 --allow-guests: reachable unauthenticated from the container network with full access to the captured-credential archive. Now binds 127.0.0.1 inside the container. - Source bind mounts now default read_only=True; opt-in no-new-privileges security_opt; memory (4g) and pids (512) limits now default-on; the host.docker.internal gateway is now opt-out. Dockerfile supply-chain pinning TODOs added (no fabricated digests/SHAs). Host-side execution (interface/, core/): - HIGH: git clone accepted ext:: URLs -> host RCE via the remote helper. Scheme allowlist (http/https/ssh/git@ only), reject ::/-, and clone with -c protocol.ext.allow=never -c protocol.allow=user -- . - HIGH: --resume re-mounted workspace_mount from an agent-writable run.json with only an is_dir() check -> host / mounted read-write. Now revalidated with check_mountable_dir(); run name sanitized. - Run artifacts (transcript + harvested secrets) were world-readable in CWD with no .gitignore. Run dir 0700, artifacts 0600, strix_runs/.gitignore. Proxy / prompt-injection (tools/proxy/, agents/, core/): - HIGH: no code-level scope enforcement. repeat_request now validates the destination host: deny-by-default RFC1918 / link-local / 169.254.169.254 metadata / loopback / host.docker.internal unless in the authorized set (threaded from the agent factory; hostnames DNS-resolved before the check). - HIGH: untrusted tool output shared the operator's instruction channel. Target-derived output (proxy, web_search, exec/stdin) now wrapped in a per-run nonce provenance envelope; system prompt treats its contents as data; forged control markers stripped from peer-agent message bodies. - CRLF/header injection in build_raw_request rejected (explicit allow_crlf escape hatch for intentional PoCs); sub-agent fan-out capped (32 live / depth 5); respawn skill names validated; STRIX_CAIDO_URL pinned loopback. Viewer web server (interface/viewer/): - /api/event now session-gated; Content-Length bounded (reject <0 / >1MiB); Host/Origin validation closes DNS-rebinding; socket timeout 30s; run count and steer capability gated; nosniff/no-store/CSP/Referrer-Policy headers; javascript: URLs no longer rendered as anchors in the transcript. Verified with a dynamic attack harness: 15/15 exploits now blocked, the legitimate operator flow unaffected. Supply chain / secrets (scripts/, config/, telemetry/): - install.sh verifies a published checksum and fails closed on mismatch; removed the rm -f of foreign `strix` binaries on PATH. Self-update fails closed when no digest is available instead of installing unverified. litellm pinned >=1.90,<2. API keys no longer persisted to plaintext config by default. Telemetry first-run marker no longer written when telemetry is disabled. Remaining (release-workflow, out of tree): publish SHA256SUMS + cosign/ build-provenance signing so install.sh and self-update can verify a publisher signature, not just a same-origin hash. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An adversarial re-review of the first wave (25946aa) plus a pass over the surface it did not cover (Go TUI backend, api_spec, report) surfaced holes the first fixes left open. This closes them. Egress / SSRF (tools/proxy): - DNS-rebinding TOCTOU: the scope guard resolved the host but the request re-resolved the hostname at send time. Now resolves once and pins the validated IP into the connection; the Host header keeps the original name. - Framed honestly as defense-in-depth for the repeat_request path only — exec_command / browser egress is enforced at the container network layer, not by a per-tool check. - _AUTHORIZED_HOSTS is now replace-semantics per scan (was a process-global that accumulated across runs). - CRLF/header-injection and STRIX_CAIDO_URL loopback pin retained. Prompt-injection provenance (agents/factory, output_store): - The untrusted-output envelope now also wraps filesystem read results, the channel the first wave missed. Sandbox egress (containers/docker-entrypoint.sh, runtime/docker_client): - Drop egress to cloud-metadata endpoints (169.254.169.254, ECS 169.254.170.2, IPv6 fd00:ec2::254) via iptables at container start; RFC1918 left reachable (local targets are legitimate). Disable with STRIX_ALLOW_METADATA=1. Supply chain (scripts/install.sh, .github/workflows/build-release.yml): - install.sh now fails CLOSED when a checksum cannot be verified (was warn-and-proceed); escape hatch STRIX_INSTALL_SKIP_VERIFY=1. - The release workflow now generates and publishes SHA256SUMS so the fail-closed install has a manifest to verify against. Host exec (interface/utils.py): - Reject a clone host beginning with '-' (ssh ProxyCommand injection); the --depth over-spec was dropped so --diff-base history scans still work. Viewer (interface/viewer/server.py): - Host/Origin guard now fails closed (missing Host, null Origin, wrong-port Host all rejected) on the loopback bind; non-loopback --host relies on the session cookie. Re-verified with the attack harness: 18/18 blocked. Run-dir perms (core/paths.py, core/sessions.py, report/writer.py): - Create dirs 0700 via mkdir(mode=) so there is no world-readable window; the session sqlite db is touched 0600 before open. Uncovered surface (utils/api_spec.py, report/writer.py): - YAML spec loading now caps file size and refuses anchor/alias expansion (billion-laughs) via a SafeLoader subclass. - CSV report cells are neutralized against spreadsheet formula injection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Greptile SummaryThis PR hardens sandbox isolation, host-side target handling, proxy replay, prompt provenance, viewer access, artifact permissions, configuration persistence, parsing, reporting, and release integrity.
Confidence Score: 3/5This PR should not merge until Caido remains reachable from the host-side client and valid release checksums can pass installer verification. The loopback listener breaks the endpoint used by host-side proxy bootstrap, while the checksum filter rejects the exact manifest format emitted by the release workflow, blocking ordinary installations. Files Needing Attention: containers/docker-entrypoint.sh, strix/runtime/docker_client.py, strix/runtime/session_manager.py, scripts/install.sh, .github/workflows/build-release.yml Important Files Changed
Prompt To Fix All With AI### Issue 1
containers/docker-entrypoint.sh:84
**Loopback bind breaks Caido bootstrap**
When a scan initializes the host-side Caido client, the runtime connects to the container's bridge-network address while Caido listens only on container loopback, causing SDK connection and project setup to fail and leaving proxy tools and operator access unavailable.
### Issue 2
scripts/install.sh:198-199
**Manifest filter rejects valid checksums**
When installing from the generated `SHA256SUMS`, plain `sha256sum` separates the digest and filename with two spaces, but this expression accepts only one whitespace character before the filename, causing every normally verified installation to report that the entry is missing and abort.
```suggestion
if awk -v file="$file" '$2 == file || $2 == "*" file' SHA256SUMS > SHA256SUMS.filtered 2>/dev/null \
&& [ -s SHA256SUMS.filtered ]; then
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "security: second-wave fixes — close bypa..." | Re-trigger Greptile |
| # other container on the same Docker bridge network. --allow-guests is retained | ||
| # because the local Python client authenticates via loginAsGuest; the loopback | ||
| # bind is what removes the container-network exposure. | ||
| caido-cli --listen 127.0.0.1:${CAIDO_PORT} \ |
There was a problem hiding this comment.
Loopback bind breaks Caido bootstrap
When a scan initializes the host-side Caido client, the runtime connects to the container's bridge-network address while Caido listens only on container loopback, causing SDK connection and project setup to fail and leaving proxy tools and operator access unavailable.
Knowledge Base Used:
Prompt To Fix With AI
This is a comment left during a code review.
Path: containers/docker-entrypoint.sh
Line: 84
Comment:
**Loopback bind breaks Caido bootstrap**
When a scan initializes the host-side Caido client, the runtime connects to the container's bridge-network address while Caido listens only on container loopback, causing SDK connection and project setup to fail and leaving proxy tools and operator access unavailable.
**Knowledge Base Used:**
- [Runtime backends and sessions](https://app.greptile.com/strix-org-3/-/custom-context/knowledge-base/usestrix/strix/-/docs/runtime-backends-and-sessions.md)
- [Proxy and Caido integration](https://app.greptile.com/strix-org-3/-/custom-context/knowledge-base/usestrix/strix/-/docs/proxy-and-caido-integration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| if grep -E "[[:space:]]\*?${file}\$" SHA256SUMS > SHA256SUMS.filtered 2>/dev/null \ | ||
| && [ -s SHA256SUMS.filtered ]; then |
There was a problem hiding this comment.
Manifest filter rejects valid checksums
When installing from the generated SHA256SUMS, plain sha256sum separates the digest and filename with two spaces, but this expression accepts only one whitespace character before the filename, causing every normally verified installation to report that the entry is missing and abort.
| if grep -E "[[:space:]]\*?${file}\$" SHA256SUMS > SHA256SUMS.filtered 2>/dev/null \ | |
| && [ -s SHA256SUMS.filtered ]; then | |
| if awk -v file="$file" '$2 == file || $2 == "*" file' SHA256SUMS > SHA256SUMS.filtered 2>/dev/null \ | |
| && [ -s SHA256SUMS.filtered ]; then |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/install.sh
Line: 198-199
Comment:
**Manifest filter rejects valid checksums**
When installing from the generated `SHA256SUMS`, plain `sha256sum` separates the digest and filename with two spaces, but this expression accepts only one whitespace character before the filename, causing every normally verified installation to report that the entry is missing and abort.
```suggestion
if awk -v file="$file" '$2 == file || $2 == "*" file' SHA256SUMS > SHA256SUMS.filtered 2>/dev/null \
&& [ -s SHA256SUMS.filtered ]; then
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
Thanks for the review — both addressed in Issue 1 (Caido loopback bind) — valid, fixed. Confirmed the host-side SDK reaches Caido via the container's bridge IP ( Issue 2 (checksum manifest filter) — the stated failure doesn't reproduce, but I improved it anyway. I tested the existing |
Issue 1 (REAL) — Caido loopback bind broke the host proxy bootstrap. The host-side SDK reaches Caido via the container's BRIDGE IP (docker_client resolves NetworkSettings.IPAddress; session_manager calls resolve_exposed_port), not via container loopback. Binding --listen 127.0.0.1 made Caido listen only on container-loopback, so the host client could not connect and proxy tools/operator access failed. Reverted to --listen 0.0.0.0 with a comment explaining the bind cannot itself remove the sibling-container exposure — that is contained by an isolated sandbox network plus host-loopback-only port publishing. Marked "do not re-harden to 127.0.0.1". Issue 2 (robustness, not the stated failure) — checksum filename match. Greptile reported the two-space text-mode SHA256SUMS format would be rejected; testing shows the existing grep matched both one- and two-space formats, so that specific failure does not occur. The genuine weakness is that the filename was interpolated into a grep regex, so '.' in an asset name matches any char and could select the wrong line. Switched to an awk exact-field match ($2 == file, or "*"file for binary mode) — strictly correct for a checksum-verification path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
A static security audit of the tree surfaced containment gaps in an otherwise sound design (no docker.sock mount, no privileged containers, LLM keys never enter the sandbox, SDK tracing hard-disabled). This PR fixes them in two waves — the first round of fixes, then a round closing bypasses found in an adversarial re-review.
Severity found: 1 CRITICAL, 7 HIGH, plus medium/low.
What's fixed
Sandbox / container
0.0.0.0 --allow-guests(unauthenticated access to the captured-credential archive from the container network) → bound127.0.0.1.no-new-privileges; memory/pids limits default-on; metadata egress (169.254.169.254, ECS, IPv6) dropped at container start.Host-side execution
git cloneacceptedext::URLs → host RCE via the remote helper. Scheme allowlist +protocol.ext.allow=never+--; reject a host beginning with-(ssh ProxyCommand).--resumere-mountedworkspace_mountfrom an agent-writablerun.jsonwith only anis_dir()check → host/mounted read-write. Now revalidated..gitignore→ 0700 dirs, 0600 files,strix_runs/.gitignore.Proxy / prompt-injection
repeat_requestnow validates the destination (deny-by-default RFC1918 / link-local / metadata / loopback unless in scope), resolves once and pins the validated IP to close a DNS-rebinding TOCTOU. Framed as defense-in-depth for that path; container-network egress is the real boundary.STRIX_CAIDO_URLpinned to loopback.Viewer web server
/api/eventsession-gated;Content-Lengthbounded; Host/Origin guard (fails closed) closes DNS-rebinding; socket timeout; security headers;javascript:URLs no longer rendered. Verified with a dynamic attack harness — 18/18 exploits blocked, legit operator flow unaffected.Supply chain / secrets / uncovered surface
install.shverifies a published checksum and fails closed; the release workflow now publishesSHA256SUMS. Self-update fails closed with no digest.litellmpinned. API keys no longer persisted to plaintext by default. Telemetry marker not written when disabled. YAML spec loading capped + alias-bomb-guarded; CSV report cells neutralized against formula injection.Not in this PR (recommended next)
install.shand self-update verify a signature, not just a same-origin hash.Commits:
25946aa(first wave),e967e39(second wave). Every touched Python file compiles; shell scripts passbash -n; the viewer fixes are dynamically verified.