Summary
/api/invoices/4821.Request & response
The test request, sent as the tenant-north member, returns a record owned by tenant-south.
GET /api/invoices/4821 HTTP/1.1 Host: app.example.test Authorization: Bearer [identity: member/north · masked] Accept: application/json
HTTP/1.1 200 OK Content-Type: application/json { "id": 4821, "workspace": "tenant-south", "total": [redacted], … }
Control test & reproduction
A control proves the boundary exists, and that the identity — not a missing check — is what crossed it. The result is then reproduced in a clean session before it is reported.
control → GET /api/invoices/4821 · no session 401 Unauthorized · boundary present owner → GET /api/invoices/4821 · as member/tenant-south 200 OK · owner marker matches replay → GET /api/invoices/4821 · as member/north, clean session 200 OK ✓ reproduced ×3
A 401, 429, 5xx or WAF response during reproduction keeps a candidate inconclusive. It is never promoted to a finding.
Evidence hash chain
Each exchange is appended to a hash-chained ledger, so any later change to the record is detectable.
3 requests · append-only ledger · secrets masked before storage
Fix direction & mapping
Scope invoice lookups by the caller’s workspace on the server, not only by invoice ID. Return the same “not found” response for another tenant’s invoice as for a missing one. Add a regression test that requests a foreign-workspace invoice as a member.
Retest
After the fix ships, the original attack re-runs unchanged — so a closed issue is verified, not assumed.
GET /api/invoices/4821 · as member/north 404 Not Found · boundary enforced
✓ Fixed · verified by retest
Coverage notes
Reports state what was not proven as clearly as what was. In this sample run, two candidates hit rate limiting during reproduction and are listed as inconclusive — not as findings, and not as passed.
No finding never means secure. The report names the classes that could not be tested and why.