Skip to content

security: fix audit findings (1 critical, 7 high, med/low) - #1154

Open
omranalamri wants to merge 3 commits into
usestrix:mainfrom
omranalamri:security/audit-fixes
Open

security: fix audit findings (1 critical, 7 high, med/low)#1154
omranalamri wants to merge 3 commits into
usestrix:mainfrom
omranalamri:security/audit-fixes

Conversation

@omranalamri

Copy link
Copy Markdown

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

  • CRITICAL — the MITM root CA private key was generated at image build time, so every user of the published sandbox image shared one key that signs all intercepted TLS. Moved to per-container runtime generation with a random per-run p12 password.
  • Caido proxy bound 0.0.0.0 --allow-guests (unauthenticated access to the captured-credential archive from the container network) → bound 127.0.0.1.
  • Source bind mounts default read-only; opt-in no-new-privileges; memory/pids limits default-on; metadata egress (169.254.169.254, ECS, IPv6) dropped at container start.

Host-side execution

  • HIGHgit clone accepted ext:: URLs → host RCE via the remote helper. Scheme allowlist + protocol.ext.allow=never + --; reject a host beginning with - (ssh ProxyCommand).
  • HIGH--resume re-mounted workspace_mount from an agent-writable run.json with only an is_dir() check → host / mounted read-write. Now revalidated.
  • Run artifacts (transcript + harvested secrets) were world-readable in CWD with no .gitignore → 0700 dirs, 0600 files, strix_runs/.gitignore.

Proxy / prompt-injection

  • HIGH — no code-level egress check; repeat_request now 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.
  • HIGH — untrusted tool output shared the operator's instruction channel; wrapped target-derived output (proxy, web_search, exec, filesystem reads) in a per-run nonce provenance envelope + system-prompt rule.
  • CRLF/header-injection rejected; sub-agent fan-out capped; STRIX_CAIDO_URL pinned to loopback.

Viewer web server

  • /api/event session-gated; Content-Length bounded; 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.sh verifies a published checksum and fails closed; the release workflow now publishes SHA256SUMS. Self-update fails closed with no digest. litellm pinned. 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)

  • Publisher signing (cosign / build provenance) so install.sh and 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 pass bash -n; the viewer fixes are dynamically verified.

omranalamri and others added 2 commits August 24, 2026 12:38
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-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens sandbox isolation, host-side target handling, proxy replay, prompt provenance, viewer access, artifact permissions, configuration persistence, parsing, reporting, and release integrity.

  • Generates a per-container MITM CA and adds container resource and egress controls.
  • Validates clone, resume, URL, header, viewer-session, and API-spec boundaries.
  • Protects tool-output provenance, run artifacts, secrets, telemetry state, and CSV exports.
  • Adds release checksums and fail-closed installer/self-update verification.

Confidence Score: 3/5

This 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

Filename Overview
containers/docker-entrypoint.sh Adds runtime CA generation and metadata blocking, but the loopback-only Caido bind is incompatible with the host endpoint resolved by the Docker runtime.
strix/runtime/docker_client.py Adds sandbox controls while continuing to resolve exposed services through the container network IP, which makes the new Caido listener unreachable.
strix/runtime/session_manager.py Applies mount and runtime changes, then passes the container-IP endpoint to host-side Caido bootstrap.
scripts/install.sh Adds fail-closed checksum verification, but its manifest regex rejects the standard output produced by the release workflow.
.github/workflows/build-release.yml Generates a checksum manifest for release assets; its normal sha256sum format exposes the installer filtering defect.
strix/tools/proxy/tools.py Adds destination validation, DNS pinning, and request-header safeguards without an accepted finding.
strix/interface/viewer/server.py Adds session, request-size, Host/Origin, timeout, and security-header protections without an accepted finding.
strix/interface/cli_args.py Revalidates persisted resume paths and workspace files without a demonstrated regression.
strix/agents/factory.py Wraps target-controlled tool results and registers authorized hosts without an accepted finding.
strix/utils/api_spec.py Adds bounded YAML loading and alias controls without an accepted finding.
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

Comment thread containers/docker-entrypoint.sh Outdated
# 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} \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment thread scripts/install.sh Outdated
Comment on lines +198 to +199
if grep -E "[[:space:]]\*?${file}\$" SHA256SUMS > SHA256SUMS.filtered 2>/dev/null \
&& [ -s SHA256SUMS.filtered ]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Suggested change
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.

@omranalamri

Copy link
Copy Markdown
Author

Thanks for the review — both addressed in 2e489b7.

Issue 1 (Caido loopback bind) — valid, fixed. Confirmed the host-side SDK reaches Caido via the container's bridge IP (docker_client resolves NetworkSettings.IPAddress; session_manager.resolve_exposed_port), not container loopback, so --listen 127.0.0.1 made it unreachable. Reverted to --listen 0.0.0.0 and documented that the bind can't remove the sibling-container exposure — that's contained by an isolated sandbox network + host-loopback-only publishing — with a "do not re-harden to 127.0.0.1" note so it isn't reintroduced.

Issue 2 (checksum manifest filter) — the stated failure doesn't reproduce, but I improved it anyway. I tested the existing grep -E "[[:space:]]\*?${file}$" against a real two-space text-mode SHA256SUMS line and it matched (grep is a substring match, so the count of leading spaces doesn't matter). So installs weren't actually aborting on that. The real weakness is that the filename was interpolated into a grep regex. 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), which is strictly correct for a checksum-verification path. Same effect as your suggestion.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant