- Endpoint
- GET /api/v2/invoices/:id · api.ledgerly.io
- Finding ID
- ZD-c518efe09c
- Detected by
- bola · canary-confirmed
- CVSS 4.0 vector
- AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
What it means
The invoice API checks that you are signed in, but not that the invoice belongs to your company. Any customer can read any other customer’s invoice by changing the ID.
How an attacker uses it
Sign up for a free trial, then walk invoice IDs. The IDs are short and sequential per tenant, and the staging admin (finding 03) lists them for every tenant.
Business impact
Invoices include payroll runs: employee names, salaries and bank details for every ledgerly.io customer. Exposure of this data would start the GDPR 72-hour notification clock.
Proof · attack vs control
Attack · tenant A’s session
GET /api/v2/invoices/inv_B0042 HTTP/1.1
Host: api.ledgerly.io
Authorization: Bearer ‹tenant A token, redacted›
200OK · tenant B’s invoice
{"id":"inv_B0042","tenant_id":"tnt_8c1f",
"type":"payroll_run","lines":14,
"memo":"zdiq-canary-7f3a"}
differs from
Control · no session
GET /api/v2/invoices/inv_B0042 HTTP/1.1
Host: api.ledgerly.io
(no Authorization header)
401Unauthorized
{"error":"unauthorized"}
curl -i https://api.ledgerly.io/api/v2/invoices/inv_B0042 \
-H "Authorization: Bearer $TENANT_A_TOKEN"
Re-fired 3×, identical result. Tenant B is our second test tenant: the scan read only the canary invoice it planted, never real customer data.
The fix
src/api/invoices.ts
router.get('/invoices/:id', requireSession, async (req, res) => {
- const invoice = await Invoice.findById(req.params.id);
+ const invoice = await Invoice.findOne({
+ id: req.params.id,
+ tenantId: req.session.tenantId, // scope every lookup to the caller
+ });
if (!invoice) return res.sendStatus(404);
Return 404, not 403, so IDs from other tenants can’t be confirmed. Apply the same scope to /exports and /payslips, which share the lookup.
Mapped to
- OWASPA01:2021 Broken Access Control
- SOC 2CC6.1, CC6.3
- ISO 27001A.5.15, A.8.3
- PCI DSS 4.07.2.1
- All 7 frameworks
Retest
Open
Ship the fix, then retest from the report. The same attack and control are re-fired.