An invoice is returned to another account.
A fictional example of how a finding is documented, from evidence to retest. Domains, accounts and data are invented.
Scope
In scope: app.example.com and api.example.com/v1, tested with two authorized test accounts. Excluded by agreement: third-party payment pages. Authentication was confirmed during scoping, and no source code was needed for this external test.
Test accounts
session=test-account-a
Owns invoice demo-01
session=test-account-b
Owns invoice demo-02
Request and response
Signed in as test account A, we requested an invoice that belongs to test account B.
GET /api/invoices/demo-02 HTTP/1.1 Host: api.example.com Cookie: session=test-account-a
{ "invoice": "demo-02",
"owner": "test account B"}The API returned account B's invoice to account A. A researcher reproduced the result before reporting it.
Steps to reproduce
- Sign in to app.example.com as test account A.
- Send GET /api/invoices/demo-02 with account A's session.
- Observe a 200 response containing account B's invoice.
- Repeat as account B to confirm that demo-02 belongs to B.
Code context Proposed Defense scope
Trace: web app, then billing service, then the invoice lookup. The handler loads an invoice by id and returns it without checking its owner.
export async function getInvoice(req, res) {
const invoice = await invoices.find(req.params.id)
return res.json(invoice) // owner not compared}Suggested approach: compare the invoice owner with the signed-in account before returning it.
Developer review
Your developers review the guidance and decide on the change. One possible version:
const invoice = await invoices.find(req.params.id) + if (!invoice || invoice.accountId !== req.session.accountId) {+ return res.status(403).end()+ } return res.json(invoice)
Retest expectation
After your team's correction, we repeat the same authorized request within scope.
Account A's own invoices should still return 200.403