reposix is, by construction, a textbook lethal-trifecta machine: it puts (1) private data (issue bodies, custom fields, attachments) on the same path as (2) untrusted input (every byte that came from the network is attacker-influenced text) and (3) exfiltration (git push is a side-effecting verb; the helper makes outbound HTTP). The design does not pretend any leg is missing — it cuts the path between them at every boundary.
The full analysis lives in docs/how-it-works/trust-model.md (user-facing) and .planning/research/v0.1-fuse-era/threat-model-and-critique.md (full historical red-team report). Read those before proposing changes that touch egress, audit, or sanitization.
These are not aspirational. Each is wired into the type system, a clippy lint, or a runtime check, and each has at least one regression test.
| Guardrail | Where |
|---|---|
Egress allowlist — every HTTP client built via reposix_core::http::client(); raw reqwest::Client::new() is banned by clippy disallowed-methods. Default origin allowlist: http://127.0.0.1:*. Override with REPOSIX_ALLOWED_ORIGINS. |
crates/reposix-core/src/http.rs |
Frontmatter field allowlist — server-controlled fields (id, created_at, version, updated_at) are stripped from inbound writes before the REST call. An attacker-authored body cannot poison server metadata. |
helper push handler; audited as helper_push_sanitized_field |
Tainted<T> / Untainted<T> newtypes — the cache returns Tainted<Vec<u8>>; sanitize() is the only safe path to a side-effecting call. A trybuild compile-fail test asserts you cannot send Tainted<T> to an egress sink without sanitize. |
crates/reposix-core/src/tainted.rs |
Append-only audit log — BEFORE UPDATE / BEFORE DELETE triggers on audit_events_cache raise an error. SQLite WAL mode. |
crates/reposix-cache/src/cache_schema.sql |
Blob-fetch limit — helper refuses any command=fetch carrying more want lines than REPOSIX_BLOB_LIMIT (default 200). Error message tells the agent to narrow scope via git sparse-checkout. |
helper; audited as blob_limit_exceeded |
Push-time conflict detection — helper checks backend version at push time; rejects stale-base pushes with error refs/heads/main fetch first. Prevents blind overwrite of writes that landed between clone and push. |
helper; audited as helper_push_rejected_conflict |
The audit-log ops vocabulary is fixed and documented in docs/how-it-works/trust-model.md.
Please do not open a public GitHub issue for security reports.
Preferred channel:
- GitHub Security Advisories — open a private advisory at https://github.com/reubenjohn/reposix/security/advisories/new. This routes directly to the maintainer with no public visibility.
Fallback channel:
- Email —
reubenvjohn@gmail.com. Subject line:[reposix security] <one-line summary>.
A PGP key is not currently published; if you need one, request it via the email above and we'll set one up. Use a GitHub Security Advisory in the meantime.
- A description of the issue, including the leg(s) of the lethal trifecta it crosses.
- Steps to reproduce (a minimal
cargo testorbashreproducer is ideal). - The affected version (
cargo pkgidoutput or git commit SHA). - Your assessment of impact — read access? write access? credential leakage? audit-log tampering?
We aim to acknowledge security reports within 7 days. For confirmed vulnerabilities, we'll work with you on a coordinated-disclosure timeline (typical: fix released before public details, 30–90 days depending on severity). Reporters who wish to be credited will be listed in Acknowledgments once the fix ships.
Each guardrail in the table above is already regression-covered today, just not yet behind a single named entrypoint: bash quality/gates/agent-ux/dark-factory.sh sim exercises the egress-allowlist + audit-log guardrails end-to-end as part of the dark-factory suite; the catalogued security gates (quality/gates/security/allowlist-enforcement.sh, audit-immutability.sh, connector-audit-wired.sh, cargo-audit-posture.sh) each verify one cut; and the unit tests in crates/reposix-core/src/tainted.rs + crates/reposix-cache/ cover the type-system and SQL invariants. A single scripts/security-regression.sh umbrella that chains all of these behind one command is not yet implemented — tracked as a nice-to-have in .planning/milestones/v0.14.0-phases/GOOD-TO-HAVES.md; until it lands, run the individual gates above (or python3 quality/runners/run.py --cadence pre-push, which composes the security-dimension rows).
Cargo dependencies are pinned in Cargo.lock (committed). cargo-audit is wired in two places: the CI Security-audit workflow (.github/workflows/audit.yml) runs cargo audit on every Cargo.toml/Cargo.lock change, on a weekly Monday cron (to pick up freshly published advisories), and on workflow_dispatch; and the catalogued local gate quality/gates/security/cargo-audit-posture.sh (catalog row security/cargo-audit-rustsec-posture, cadences pre-push + pre-release) runs the same audit plus the version-floor + posture-doc asserts described in the next section. cargo-deny (license/ban policy) is not yet wired — dependency advisories are covered by the two cargo-audit legs above; license/ban-list gating remains a tracked nice-to-have.
GitHub Actions versions are pinned via Dependabot configuration (.github/dependabot.yml) so a workflow update is always a reviewable PR.
Current status: 0 live advisories. As of 2026-07-12 (advisory-db HEAD 6e3286f4, synced same day), cargo audit scans the 419-crate Cargo.lock and reports zero vulnerabilities (exit 0). Committed evidence — full command transcript, cargo tree reachability, and advisory metadata: .planning/milestones/v0.14.0-phases/evidence/p107-cargo-audit-2026-07-12.txt.
Two advisories surfaced during the v0.13.0 → v0.14.0 window and were investigated to a definitive verdict. Neither is a direct dependency of any reposix crate, and both are cleared by version floor — not merely unreported:
| Advisory | Crate | Installed | Patched floor | Reachability | Verdict |
|---|---|---|---|---|---|
| RUSTSEC-2026-0185 | quinn-proto |
0.11.15 |
>= 0.11.15 |
transitive-and-absent — cargo tree -i quinn-proto prints "nothing to print" under all targets/features; quinn / quinn-proto / quinn-udp survive only as orphan Cargo.lock entries (unreachable, never built) |
Not actionable. Floor already met and the crate is not in the resolved build. |
| RUSTSEC-2026-0186 | memmap2 |
0.9.11 |
>= 0.9.11 |
transitive-and-present via gix 0.83.0 (gix-commitgraph/-index/-odb/-pack/-ref) → reposix-cache → reposix-cli & reposix-remote |
Not actionable. Floor already met; the advisory is informational = "unsound", gated only by -D unsound (it does not fail a default cargo audit even when unpatched). |
Mitigation, in three layers: (1) the committed Cargo.lock pins both crates at or above their patched floors; (2) the CI audit.yml workflow re-runs cargo audit on every lockfile change and weekly, so a future regression (a lockfile regen pulling an unpatched floor, or a newly published advisory) trips CI; (3) the local catalogued gate security/cargo-audit-posture.sh re-asserts the audit is clean and independently verifies the memmap2 / quinn-proto floors from Cargo.lock and that this section still names both advisory ids — so a green audit alone cannot mask a silent floor regression. A network-flaky advisory-db fetch is reported as indeterminate (gate exit 2 / PARTIAL), never conflated with a live advisory.
Credential leakage is caught in two independent layers so neither is a single point of failure:
- Layer 1 — zero-dependency grep gate (local, pre-push).
quality/gates/structure/cred-hygiene.shgreps the outgoing ref-range for a fixed set of credential prefixes (AtlassianATATT3, GitHubghp_/github_pat_, GoogleAIza, AWSAKIA, Slackxox[baprs]-, OpenAIsk-, PEM private-key headers). It is invoked by the.githooks/pre-pushhook and needs no binary beyondbash+grep, so a fresh clone is protected the momentbash scripts/install-hooks.shruns. Narrow by design — it trades recall for zero setup. - Layer 2 — gitleaks full ruleset (CI, every push/PR). The
gitleaksjob in.github/workflows/ci.ymlruns a version-pinned, checksum-verified gitleaks binary over full history (fetch-depth: 0), governed by.gitleaks.toml. This is the broad backstop the grep gate cannot be. We pin the release binary by SHA-256 rather than using the marketplace action (no license-key requirement, smaller supply-chain surface). - Layer 0 (optional) — personal global hook. A contributor may wire their own secret scanner at
~/.git-hooks/pre-push; the project hook chains to it. This is convenience, not a guarantee — it is invisible to CI and to other contributors, which is precisely why Layer 2 exists.
The one allowlisted false positive (.playwright-mcp/ browser-automation console logs, which capture third-party pages' own client-side Google web keys) is documented inline in .gitleaks.toml. Allowlisting is by path, never by pattern, so a real key committed anywhere else still trips both layers.
Honesty about the threat model is a feature, not a footnote. The following are not mitigated by reposix and are not bugs to file:
- Shell access bypasses every cut. An attacker on the dev host can
curlthe backend with the same token. reposix is a substrate for safer agent loops, not a sandbox. The egress allowlist guards the helper and the cache; it does not guard the rest of the host. - The simulator is itself attacker-influenced. Seed data is authored by an agent (or by a fixture written by an agent), so simulator runs are also tainted. The lethal-trifecta mitigations apply against the simulator just as hard as against a real backend.
- Token leakage via crash logs. A panicking helper that includes auth headers in its
RUST_BACKTRACEoutput can leak credentials. Known credential headers are scrubbed before logging, but third-party crates panicking with a header in scope are out of reposix's hands. - Confused-deputy across backends. A user with credentials for two backends and one allowlist entry can be tricked by a tainted issue body into directing writes at the wrong backend. The allowlist constrains origin; it does not constrain intent. Multi-backend egress is high-friction by design — the agent must run a separate
reposix initper backend. - Cache compromise. An attacker with write access to
cache.dbcan replay or hide audit rows from older WAL segments. Append-only triggers prevent in-place tampering on the live segment but cannot defend against the file being swapped wholesale.
If you find a way around one of the intended mitigations, that's the report we want. If you find a way around something this list explicitly cedes, that's expected behaviour — though we're still happy to hear about novel attack chains.
Security reporters who have helped harden reposix will be listed here. (None yet — be the first.)