Deep scan
A multi-tenant billing API. Tenants are separated by org_id on every table; sessions carry the org. The riskiest surfaces are invoice downloads, the Stripe webhook and admin exports.
Completed in 5m 31s · $0.31
- Took
- 5:31
- Lines read
- 48,211
- Findings
- 3
- Cost
- $0.31
- Setup (done)
- Index (done)
- Sweep (done)
- Map (done)
- Hunt (done)
- Verify (done)
- Report (done)
00:00Setup
00:00Cloning acme/api at the default branch…
00:03Checked out main at 9f3c2e1.
00:04Index
00:04Indexing the code…
00:14Indexed 412 files, 48k lines.
00:15Sweep
00:16Sweeping for secrets, known-vulnerable packages and dangerous sinks…
00:323 leads: ws@8.16.0 is imported and has an advisory; 2 queries are built by string concatenation.
00:34Map
00:34Mapping the attack surface…
00:54Mapped 38 entry points across 6 areas: auth, invoices, webhooks, admin, members, realtime.
00:55Hunt
00:56Hunting across 6 areas in parallel…
01:12auth: session issuance and API keys, 7 entry points. Nothing exploitable.
01:38invoices: following GET /invoices/:id/pdf into the storage layer…
02:13criticalInvoice PDFs can be downloaded across tenants by changing the id · src/routes/invoices.ts:42
02:25webhooks: checking the order of parse, verify and act in the Stripe handler…
02:57highStripe webhook is acted on before its signature is checked · src/webhooks/stripe.ts:31
03:12admin: tracing orgId from the export query string to the CSV writer…
03:42highAdmin CSV export trusts the orgId in the query string · src/admin/export.ts:58
03:55members, realtime: 2 leads ruled out. Role changes are checked server-side; socket auth is enforced on upgrade.
04:03Verify
04:04Verifying 3 findings against a disposable copy of the app…
04:32Invoices: fetched a second tenant's PDF with a trial session. Verified.
04:52Webhook: an unsigned invoice.paid body was accepted. Verified.
05:08Admin export: every hop confirmed statically; runtime replay needs an admin fixture.
05:14Report
05:16Writing fix prompts and regression tests…
05:31Found 3 issues: 1 critical, 2 high. 48,211 lines read in 5m 18s.
Findings
3 findings, most severe first
- criticalInvoice PDFs can be downloaded across tenants by changing the idDetailsverified (runtime replay)
Threat model: A signed-in user of any tenant crosses the tenant boundary in the invoices route to reach every tenant's invoice PDFs.
Suggested patch
--- a/src/db/invoices.ts +++ b/src/db/invoices.ts @@ -15,5 +15,5 @@ -export async function byId(id: string) { - return db.one("select * from invoices where id = $1", [id]); +export async function byId(orgId: string, id: string) { + return db.oneOrNone("select * from invoices where id = $1 and org_id = $2", [id, orgId]); } Fix prompt
In acme/api, GET /invoices/:id/pdf (src/routes/invoices.ts:38) loads the invoice by id only (src/db/invoices.ts:17), so any signed-in user can fetch another tenant's PDF. Change byId to take the caller's orgId and filter on org_id; return 404 when no row matches. Add the regression test in test/invoices.test.ts that seeds two tenants and expects 404 across them.
Threat model: An unauthenticated caller on the internet crosses the payment provider's signature to reach subscription and payment state.
Fix prompt
In acme/api src/webhooks/stripe.ts, move stripe.webhooks.constructEvent above the dispatch, return 400 when it throws, and dispatch the event it returns instead of JSON.parse(req.body).
Threat model: An admin of any organisation crosses the organisation scope on admin routes to reach other organisations' member exports.