Captatum is a URL-fetcher that may also run a headless browser — a textbook SSRF and sandbox surface. Security is a first-class concern, not an afterthought.
Please do not open a public GitHub issue for security vulnerabilities.
Report vulnerabilities privately via GitHub's built-in channel: Security tab → "Report a vulnerability" (private vulnerability reporting). This notifies the maintainers confidentially and lets us coordinate a fix and disclosure.
If GitHub private reporting is unavailable, open a plain issue asking a maintainer to contact you privately — do not include vulnerability details in the public issue.
Please include, where possible: a description, steps to reproduce, affected component (egress/Tier-1 fetch, Tier-3 browser sandbox, OAuth, prompt-injection handling, supply chain), and impact. We aim to acknowledge reports within a few days.
Only the latest release line receives security fixes. Known-affected disclosure (2026-09-02): v0.20.2 — the newest release — is affected by a sensitive-content scanner bypass (percent-encoded credential keys on relative references egress to a hosted LLM; reproduced against v0.20.2) and by the pre-auth surface issues fixed after its release. The fixes are merged on main and ship in the next patch; if you rely on output: summary|extract against non-public content before then, prefer output: raw or a local transform provider.
Pin to a released tag; main is unreleased and may change.
docs/threat-model.md is the security reasoning and the
list of required controls; docs/contracts.md §"Security
controls" is the contract reference. Key invariants:
- SSRF: every outbound request — Tier-1, Tier-2, every redirect hop, every
Tier-3 browser subresource — routes through one rebinding-proof
guardedFetch(resolve-once → pin-to-IP → revalidate each hop; exhaustive IANA private-IP blocking). - Tier-3 browser: runs in a separate sidecar container (hosted); every browser
HTTP/subresource request is fulfilled through
guardedFetch(route.fulfill, neverroute.continue). Transport-layer egress below request interception is not reachable by it: on local, WebRTC STUN/TURN UDP is blocked by thedisable_non_proxied_udpChromium flag and browser TCP (TURN/TCP included) by a refusing loopback proxy (both verified in the headless-shell launcher; other UDP-based transports such as QUIC are unverified locally); on hosted, ALL transport egress below interception is stopped only by the browser Pod's egress firewall — a deployment prerequisite this repo does not ship (without an equivalent boundary, hosted Tier-3 is unsupported). - Auth: the hosted flavor authenticates every
/mcprequest via gateway OAuth (PKCE S256, hash-only token storage, replay-revoking refresh rotation, per-request scope enforcement). - Prompt injection: fetched content is treated as untrusted data, never instructions.
- The local-binary flavor has no authentication and must never be exposed on a network — it is single-user/single-agent, loopback only.
- Tier-1 HTTPS intentionally does not use the TLS fingerprint (it uses a checked-IP Node path to preserve rebinding-proof SSRF).
- Prompt-injection fencing applies to
summary/extract, notoutput: raw. One disclosed residual: a char-class/metachar ReDoS in extract-schema validationRetired in 0.20.2: pattern EXECUTION is now wall-clock-bounded on a worker thread and fails closed on timeout, and pattern CONTENT is rejected at the input boundary before any fetch/LLM spend (the input-boundary heuristic remains shape-based, so a heuristic-passing pattern can still burn worker time — bounded, not unbounded).
Dependencies are pinned, open-source, minimal, and held to a 15-day
minimum-release-age gate; pnpm audit --prod is required clean before any hosted
deployment (see docs/dependency-ledger.md). The
browser-sidecar base image is pinned by sha256 digest, and CI/release GitHub
Actions are pinned by commit SHA.