RepoPilot reads untrusted GitHub content. The whole design is shaped around that fact.
- 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)
- 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.
- 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.
- An unauthenticated client that wants to abuse the rate limit.
They can hammer
/api/v1/auditsfrom one IP or from a botnet. - An operator who misconfigures production by leaving a default
CORS origin, an empty
OKX_PAYMENT_ADDRESS, orPAYMENT_MODE=mock.
- 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
- The pipeline never executes the target repo's code. No
npm install, nonode, nobash, nopython, nomake, nodocker 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_BYTESper 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.tsand 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).
- The audit endpoint always goes through the configured
PaymentAdapter. The adapter is wired inbuildPaymentAdapterinpackages/okx-adapter/src/factory.ts; the route never holds the bypass. - Mock and OKX adapters share the same interface; switching is
config-only.
buildPaymentAdapterrefuses to construct anOkxPaymentAdapterunlessOKX_PAYMENT_ADDRESSis a 0x-prefixed 40-hex address, and it throws rather than falling back to mock. (This used to say the factory checksisConfigured(); it checks the address itself.isConfigured()is on the interface and is asserted by tests, but no production code path calls it.) paymentIdis the idempotency key. A replay with the samepaymentIdresolves to the same job, the same report, and the same status. That lookup belongs to the route: each POST mints a freshpaymentId(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 secondpaymentIdfor the samequoteKeythrough the route" — true of the route, false of the OKX adapter, which cached onquoteKeyand did exactly that (R-39). ThequoteKeyparameter 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 freshpaymentId. The OKX adapter therefore burns the(from, nonce)pair on first successful verification and rejects a repeat, which is what EIP-3009's on-chainauthorizationUsedmapping does and what this adapter stands in for. It also enforces the signedvalidAfter/validBeforewindow. 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_statustool returns the samepaymentIdstatus that the HTTP API exposes. The two views cannot diverge.
- 60 req/min/IP on
POST /api/v1/auditsby default (RATE_LIMIT_PER_MINUTE). - The
MAX_FILES,MAX_FILE_BYTES,MAX_TOTAL_BYTESlimits 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_TOKENto lift to 5000 req/h.
pnpm env:checkfails the deploy in production if:CORS_ORIGINSis*PAYMENT_MODE=mockDATABASE_URLstarts withfile:OKX_PAYMENT_ADDRESSis 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_KEYSinsidescripts/env-check.ts; adding a new one is a one-line change. /healthalways reports the activepaymentMode. Operators monitor this against the expected value and alert on drift.
- Pino redact covers all the obvious secret paths:
*.password,*.token,*.apiKey,*.secret,*.privateKey,*.mnemonicreq.headers.authorizationreq.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.tsposts a request with known fake secrets and asserts they do not appear in logs.
- 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, andgenerateLaunchCopy" — three methods that never existed. The interface exposedname,isConfigured()andgenerate(), 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 isscore.ts. - Every finding in the report has at least one
evidenceentry withfile,lineandreason. This is enforced at the type level byFindingSchema.
- 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.mdand are not casually bumped. Dockerfileis multi-stage and runs as a non-root user.
Please open a private issue or contact the maintainer. Do not disclose the vulnerability in a public issue until a fix is available.