Skip to content

Security: reubenjohn/reposix

SECURITY.md

Security policy

Threat model summary

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.

Hard guardrails (test-enforced)

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

Reporting a vulnerability

Please do not open a public GitHub issue for security reports.

Preferred channel:

Fallback channel:

  • Emailreubenvjohn@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.

What to include

  • A description of the issue, including the leg(s) of the lethal trifecta it crosses.
  • Steps to reproduce (a minimal cargo test or bash reproducer is ideal).
  • The affected version (cargo pkgid output or git commit SHA).
  • Your assessment of impact — read access? write access? credential leakage? audit-log tampering?

Response SLA

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.

Security regression kit

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

Supply chain

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.

Advisory posture (cargo audit / RUSTSEC)

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-absentcargo 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-cachereposix-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.

Secret scanning

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.sh greps the outgoing ref-range for a fixed set of credential prefixes (Atlassian ATATT3, GitHub ghp_ / github_pat_, Google AIza, AWS AKIA, Slack xox[baprs]-, OpenAI sk-, PEM private-key headers). It is invoked by the .githooks/pre-push hook and needs no binary beyond bash + grep, so a fresh clone is protected the moment bash scripts/install-hooks.sh runs. Narrow by design — it trades recall for zero setup.
  • Layer 2 — gitleaks full ruleset (CI, every push/PR). The gitleaks job in .github/workflows/ci.yml runs 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.

Known limitations

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 curl the 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_BACKTRACE output 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 init per backend.
  • Cache compromise. An attacker with write access to cache.db can 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.

Acknowledgments

Security reporters who have helped harden reposix will be listed here. (None yet — be the first.)

There aren't any published security advisories