Moderate severityNVD Advisory· Published Mar 6, 2026· Updated Mar 9, 2026
Kimai: API invoice endpoint missing customer-level access control (IDOR)
CVE-2026-28685
Description
Kimai is a web-based multi-user time-tracking application. Prior to version 2.51.0, "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. This issue has been patched in version 2.51.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
kimai/kimaiPackagist | < 2.51.0 | 2.51.0 |
Affected products
1Patches
1a0601c8cb28fcheck customer permissions on invoice api access (#5849)
2 files changed · +63 −0
src/API/InvoiceController.php+5 −0 modified@@ -18,6 +18,7 @@ use FOS\RestBundle\View\View; use FOS\RestBundle\View\ViewHandlerInterface; use OpenApi\Attributes as OA; +use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -76,6 +77,9 @@ public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerReposito /** @var array<int> $customers */ $customers = $paramFetcher->get('customers'); foreach ($customerRepository->findByIds(array_unique($customers)) as $customer) { + if (!$this->isGranted('access', $customer)) { + throw $this->createAccessDeniedException('Cannot access Customer: ' . $customer->getId()); + } $query->addCustomer($customer); } @@ -90,6 +94,7 @@ public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerReposito * Fetch invoice */ #[IsGranted('view_invoice')] + #[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')] #[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))] #[Route(methods: ['GET'], path: '/{id}', name: 'get_invoice', requirements: ['id' => '\d+'])] public function getAction(Invoice $invoice): Response
tests/API/InvoiceControllerTest.php+58 −0 modified@@ -9,8 +9,11 @@ namespace App\Tests\API; +use App\Entity\Customer; use App\Entity\Invoice; +use App\Entity\Team; use App\Entity\User; +use App\Repository\TeamRepository; use App\Tests\DataFixtures\InvoiceFixtures; use PHPUnit\Framework\Attributes\Group; @@ -123,4 +126,59 @@ public function testNotFound(): void { $this->assertEntityNotFound(User::ROLE_USER, '/api/invoices/' . PHP_INT_MAX); } + + public function testDownloadRespectsCustomerPermission(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + + $invoices = $this->importInvoiceFixtures(1, [Invoice::STATUS_NEW]); + $invoice = $invoices[0]; + $customer = $invoice->getCustomer(); + self::assertInstanceOf(Customer::class, $customer); + + $this->assertAccessIsGranted($client, '/api/invoices/' . $invoice->getId()); + + $content = $client->getResponse()->getContent(); + self::assertIsString($content); + $result = json_decode($content, true); + + self::assertIsArray($result); + self::assertApiResponseTypeStructure('Invoice', $result); + + $team = new Team('foo'); + $team->addTeamlead($this->getUserByRole(User::ROLE_ADMIN)); + $team->addCustomer($customer); + + $em = $this->getEntityManager(); + /** @var TeamRepository $repository */ + $repository = $em->getRepository(Team::class); + $repository->saveTeam($team); + + $this->assertApiAccessDenied($client, '/api/invoices/' . $invoice->getId()); + } + + public function testCollectionRespectsCustomerPermission(): void + { + $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD); + + $invoices = $this->importInvoiceFixtures(1, [Invoice::STATUS_NEW]); + $invoice = $invoices[0]; + $customer = $invoice->getCustomer(); + self::assertInstanceOf(Customer::class, $customer); + + $query = ['customers' => [$customer->getId()]]; + $this->assertAccessIsGranted($client, '/api/invoices', 'GET', $query); + + $team = new Team('foo'); + $team->addTeamlead($this->getUserByRole(User::ROLE_ADMIN)); + $team->addCustomer($customer); + + $em = $this->getEntityManager(); + /** @var TeamRepository $repository */ + $repository = $em->getRepository(Team::class); + $repository->saveTeam($team); + + $this->request($client, '/api/invoices', 'GET', $query); + $this->assertApiResponseAccessDenied($client->getResponse()); + } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-v33r-r6h2-8wr7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-28685ghsaADVISORY
- github.com/kimai/kimai/commit/a0601c8cb28fed1cca19051a8272425069ab758fghsax_refsource_MISCWEB
- github.com/kimai/kimai/releases/tag/2.51.0ghsax_refsource_MISCWEB
- github.com/kimai/kimai/security/advisories/GHSA-v33r-r6h2-8wr7ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.