Skip to content

Security: azen07508-debug/repopilot

Security

docs/SECURITY.md

RepoPilot — Security Model

RepoPilot reads untrusted GitHub content. The whole design is shaped around that fact.

Threat model

Assets we want to protect

  • The host running RepoPilot (the API process, the database, the filesystem, the user's secrets in env vars)
  • The integrity of the report we return to the buyer (evidence provenance, score rules)
  • The buyer's wallet, signing keys, and payment details
  • The seller's reputation (if a competitor subverts our scoring)

Adversaries we consider

  1. A target repository that wants to escape the sandbox. They can put anything in their README, source code, package.json scripts, Makefile, Dockerfile, or workflow files. They can also try prompt injection in any text field.
  2. A buyer who wants to get a report without paying. They can replay requests, fuzz the payment headers, and try to call the audit endpoint directly without going through the payment adapter.
  3. An unauthenticated client that wants to abuse the rate limit. They can hammer /api/v1/audits from one IP or from a botnet.
  4. An operator who misconfigures production by leaving a default CORS origin, an empty OKX_PAYMENT_ADDRESS, or PAYMENT_MODE=mock.

Out of scope (explicitly)

  • The buyer sending a real, large USDT payment to a wrong address (this is on the buyer's wallet; we do not custody funds)
  • A targeted attack against the host kernel or container runtime (we assume the runtime is patched)
  • Side-channel attacks against the scoring rules
  • A malicious insider with shell on the host

Mitigations

Sandbox escape via target repo content

  • The pipeline never executes the target repo's code. No npm install, no node, no bash, no python, no make, no docker build. This is enforced at the architecture level: the pipeline operates on text only.
  • Binary files are sniffed and skipped. The fetcher only reads UTF-8 / UTF-16 / ASCII text up to MAX_FILE_BYTES per file.
  • Path traversal is rejected: the fetcher refuses .., .git/, node_modules/, and dotfiles other than a small allowlist (.env.example, .gitignore, .dockerignore).
  • Prompt injection patterns are detected by security/injection.ts and reported as findings. Nothing from the target repository is ever passed to a model, because there is no model in the process: this used to read "The LLM (when enabled) is never asked to follow instructions from the target repo", which was a rule about a component that did not exist (R-38).

Payment bypass

  • The audit endpoint always goes through the configured PaymentAdapter. The adapter is wired in buildPaymentAdapter in packages/okx-adapter/src/factory.ts; the route never holds the bypass.
  • Mock and OKX adapters share the same interface; switching is config-only. buildPaymentAdapter refuses to construct an OkxPaymentAdapter unless OKX_PAYMENT_ADDRESS is a 0x-prefixed 40-hex address, and it throws rather than falling back to mock. (This used to say the factory checks isConfigured(); it checks the address itself. isConfigured() is on the interface and is asserted by tests, but no production code path calls it.)
  • paymentId is the idempotency key. A replay with the same paymentId resolves to the same job, the same report, and the same status. That lookup belongs to the route: each POST mints a fresh paymentId (D-011), and neither adapter caches one, so two buyers cannot be handed the same id. This used to read "There is no way to mint a second paymentId for the same quoteKey through the route" — true of the route, false of the OKX adapter, which cached on quoteKey and did exactly that (R-39). The quoteKey parameter is gone, so the sentence is not restatable.
  • An authorization is single-use, and the adapter enforces it. The signed EIP-712 message does not contain the paymentId, so one signature is valid for every challenge quoting the same payee and amount — and every POST mints a fresh paymentId. The OKX adapter therefore burns the (from, nonce) pair on first successful verification and rejects a repeat, which is what EIP-3009's on-chain authorizationUsed mapping does and what this adapter stands in for. It also enforces the signed validAfter / validBefore window. Both were absent until R-40, when one signature could buy unlimited audits. The set is in-process: a restart (or a second replica) forgets it and replay becomes possible again. That gap is deliberate and recorded (R-40, BACKLOG.md), not overlooked.
  • The MCP get_audit_status tool returns the same paymentId status that the HTTP API exposes. The two views cannot diverge.

Rate limiting and abuse

  • 60 req/min/IP on POST /api/v1/audits by default (RATE_LIMIT_PER_MINUTE).
  • The MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES limits are enforced at the fetcher, not at the report. The route returns 413 if any of them is exceeded.
  • GitHub anonymous rate limits (60 req/h) are documented. A production deploy should set GITHUB_TOKEN to lift to 5000 req/h.

Misconfiguration

  • pnpm env:check fails the deploy in production if:
    • CORS_ORIGINS is *
    • PAYMENT_MODE=mock
    • DATABASE_URL starts with file:
    • OKX_PAYMENT_ADDRESS is not a 0x EVM address (when OKX mode)
    • Required vars are missing
  • The script never prints secret values. The known secret keys are listed in KNOWN_SECRET_KEYS inside scripts/env-check.ts; adding a new one is a one-line change.
  • /health always reports the active paymentMode. Operators monitor this against the expected value and alert on drift.

Logging

  • Pino redact covers all the obvious secret paths:
    • *.password, *.token, *.apiKey, *.secret, *.privateKey, *.mnemonic
    • req.headers.authorization
    • req.headers["x-payment"]
    • req.headers["x-payment-signature"]
    • req.headers["x-api-key"]
    • *.xPayment
  • If a new field or header is added that may carry a secret, the redact list must be updated. The integration test in apps/api/src/tests/api.integration.test.ts posts a request with known fake secrets and asserts they do not appear in logs.

Score integrity

  • Scores are produced by packages/core/src/scoring/score.ts. The file is deterministic and rule-based.
  • No LLM can modify the score, because there is no LLM in the system. This paragraph used to read "The provider interface only exposes summarize, mergeFindings, and generateLaunchCopy" — three methods that never existed. The interface exposed name, isConfigured() and generate(), and it was read by no code path; the whole surface is gone (R-38). The guarantee is now a property of the code that exists rather than a rule about an interface that does not, which is why it is worth less as a policy and more as a fact: the only thing that produces a score is score.ts.
  • Every finding in the report has at least one evidence entry with file, line and reason. This is enforced at the type level by FindingSchema.

Supply chain

  • The lockfile is committed; CI uses pnpm install --frozen-lockfile.
  • Critical pins (zod 3.24.1, MCP SDK 1.22.0, pino 10) are documented in DECISIONS.md and are not casually bumped.
  • Dockerfile is multi-stage and runs as a non-root user.

Reporting a vulnerability

Please open a private issue or contact the maintainer. Do not disclose the vulnerability in a public issue until a fix is available.

There aren't any published security advisories