GET /invoices/:id returns any organisation’s invoice
- input-1/src/routes/invoices.ts:41-45
- input-1/src/routes/invoices.ts:47-50
- Impact
Any signed-in user reads the invoices of every other organisation: amounts, line items and billing contacts. Bound: every invoice whose id the caller knows.
- Observed
requireAuthchecks that the caller is signed in. It does not check which organisation the invoice belongs to.- Line 42 looks the invoice up by
idalone and line 44 returns it. - The sibling
DELETEhandler scopes the same lookup withorgId: req.user.orgId(line 48).
input-1/src/routes/invoices.ts41–50 router.get('/invoices/:id', requireAuth, async (req, res) => { const invoice = await db.invoice.findUnique({ where: { id: req.params.id } }); if (!invoice) return res.status(404).json({ error: 'Not found' }); res.json(invoice); }); router.delete('/invoices/:id', requireAuth, async (req, res) => { await db.invoice.deleteMany({ where: { id: req.params.id, orgId: req.user.orgId } }); res.status(204).end(); });- Counterargument
Invoice ids are UUIDs, so they cannot be guessed.
ResolvedThe sibling handler at line 48 adds the organisation predicate to the same lookup. The codebase itself does not treat the id as the permission.
- Evidence gap
- No request was run. Where ids are exposed (emails, exports, logs) was not supplied.
- Fix
Use
findFirstwithwhere: { id: req.params.id, orgId: req.user.orgId }at line 42.- Next
Apply the predicate, run the test, then check the other handlers that load by id.