CVE-2026-52820
Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass
Kimai is an open-source time tracking application. Prior to 2.57.0, PATCH /api/timesheets/{id} and POST /api/timesheets accept a user-controlled project identifier through TimesheetApiEditForm and FormTrait, and ProjectRepository::getQueryBuilderForFormType() places that identifier in an unconditional OR branch that bypasses the team access criteria. Any authenticated user with edit_own_timesheet can therefore assign an owned timesheet to a project outside the user's teams, persist unauthorized project attribution, and retrieve project and customer metadata through GET /api/timesheets/{id}?full=true. TimesheetTeamAccessValidator now checks changed project and activity associations against current team access. This issue is fixed in version 2.57.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.3
Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass
Kimai is an open-source time tracking application. Prior to 2.57.0, PATCH /api/timesheets/{id} and POST /api/timesheets accept a user-controlled project identifier through TimesheetApiEditForm and FormTrait, and ProjectRepository::getQueryBuilderForFormType() places that identifier in an unconditional OR branch that bypasses the team access criteria. Any authenticated user with edit_own_timesheet can therefore assign an owned timesheet to a project outside the user's teams, persist unauthorized project attribution, and retrieve project and customer metadata through GET /api/timesheets/{id}?full=true. TimesheetTeamAccessValidator now checks changed project and activity associations against current team access. This issue is fixed in version 2.57.0.
- Record
CVE-2026-52820- CVSS 4.0
- 5.3 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-639
- Updated
- 2026-11-02
EUVDEUVD-2026-78399
5.3
Kimai is an open-source time tracking application. Prior to 2.57.0, PATCH /api/timesheets/{id} and POST /api/timesheets accept a user-controlled project identifier through TimesheetApiEditForm and FormTrait, and ProjectRepository::getQueryBuilderForFormType() places that identifier in an unconditional OR branch that bypasses the team access criteria. Any authenticated user with edit_own_timesheet can therefore assign an owned timesheet to a project outside the user's teams, persist unauthorized project attribution, and retrieve project and customer metadata through GET /api/timesheets/{id}?full=true. TimesheetTeamAccessValidator now checks changed project and activity associations against current team access. This issue is fixed in version 2.57.0.
- Record
EUVD-2026-78399- CVSS 4.0
- 5.3 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-vrr2-g9gh-c3jc
5.3
Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass
Summary
The Timesheet API PATCH /api/timesheets/{id} and POST /api/timesheets endpoints accept a user-supplied project ID and resolve it through a Symfony EntityType whose query_builder allows the submitted ID to satisfy the access predicate via an unconditional OR branch. As a result, any authenticated user can re-assign their own timesheet to any project in the database — including projects that belong to teams or customers they have no membership in and cannot otherwise see. The user can then read serialized project/customer details via GET /api/timesheets/{id}?full=true, leaking metadata (name, currency, customer hierarchy) that would otherwise be filtered out by the team ACL.
Details
Entry point — only ownership is checked in src/API/TimesheetController.php:317-355
#[IsGranted('edit', 'timesheet')]
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Timesheet $timesheet): Response
{
...
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [...]);
$form->setData($timesheet);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) { ... }
$this->service->saveTimesheet($timesheet);
...
}
src/Voter/TimesheetVoter.php:134-142:
if ($subject->getUser()?->getId() === $user->getId()) {
return $this->permissionManager->hasRolePermission($user, $permission . '_own_timesheet');
}
if (!$this->permissionManager->checkTeamAccessTimesheet($subject, $user)) {
return false;
}
For an own-timesheet, only edit_own_timesheet is required. The voter does not look at the new project being submitted; it only validates the existing record's ownership.
Form replays user-controlled project ID into the access query
src/Form/TimesheetEditForm.php:60-71:
$isNew = true;
if (isset($options['data']) && $options['data'] instanceof Timesheet) {
...
if (null !== $entry->getId()) {
$isNew = false;
}
...
}
$this->addProject($builder, $isNew, $project, $customer);
src/Form/FormTrait.php:59-100:
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options): void {
$data = $event->getData();
$customer = \array_key_exists('customer', $data) && $data['customer'] !== '' ? $data['customer'] : null;
$project = \array_key_exists('project', $data) && $data['project'] !== '' ? $data['project'] : $project;
$event->getForm()->add('project', ProjectType::class, array_merge($options, [
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
$project = \is_string($project) ? (int) $project : $project;
...
if ($isNew && \is_int($project)) {
$project = $repo->find($project);
if ($project !== null) {
if (!$project->getCustomer()->isVisible()) { ... $project = null; }
elseif (!$project->isVisible()) { $project = null; }
}
}
...
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setWithCustomer(true);
return $repo->getQueryBuilderForFormType($query);
},
]));
}
);
Two problems compound:
- The visibility re-check on line 73 is gated on
$isNew. For PATCH,$isNew = false, so the closure passes the attacker-supplied ID straight through. - Even when
$isNew = true(POST), the re-check only validatesisVisible()— it does not validate team membership.
The query-builder unconditionally accepts the submitted ID
…
- Record
GHSA-vrr2-g9gh-c3jc- CVSS 4.0
- 5.3 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-639
- Updated
- 2026-11-02
NVD
5.3
Kimai is an open-source time tracking application. Prior to 2.57.0, PATCH /api/timesheets/{id} and POST /api/timesheets accept a user-controlled project identifier through TimesheetApiEditForm and FormTrait, and ProjectRepository::getQueryBuilderForFormType() places that identifier in an unconditional OR branch that bypasses the team access criteria. Any authenticated user with edit_own_timesheet can therefore assign an owned timesheet to a project outside the user's teams, persist unauthorized project attribution, and retrieve project and customer metadata through GET /api/timesheets/{id}?full=true. TimesheetTeamAccessValidator now checks changed project and activity associations against current team access. This issue is fixed in version 2.57.0.
- Record
CVE-2026-52820- CVSS 4.0
- 5.3 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-639
- Updated
- 2026-11-02
OSV
5.3
Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass
Kimai is an open-source time tracking application. Prior to 2.57.0, PATCH /api/timesheets/{id} and POST /api/timesheets accept a user-controlled project identifier through TimesheetApiEditForm and FormTrait, and ProjectRepository::getQueryBuilderForFormType() places that identifier in an unconditional OR branch that bypasses the team access criteria. Any authenticated user with edit_own_timesheet can therefore assign an owned timesheet to a project outside the user's teams, persist unauthorized project attribution, and retrieve project and customer metadata through GET /api/timesheets/{id}?full=true. TimesheetTeamAccessValidator now checks changed project and activity associations against current team access. This issue is fixed in version 2.57.0.
- Record
CVE-2026-52820- CVSS 4.0
- 5.3 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-639
- Updated
- 2026-11-02
OSVGHSA-vrr2-g9gh-c3jc
5.3
Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass
Summary
The Timesheet API PATCH /api/timesheets/{id} and POST /api/timesheets endpoints accept a user-supplied project ID and resolve it through a Symfony EntityType whose query_builder allows the submitted ID to satisfy the access predicate via an unconditional OR branch. As a result, any authenticated user can re-assign their own timesheet to any project in the database — including projects that belong to teams or customers they have no membership in and cannot otherwise see. The user can then read serialized project/customer details via GET /api/timesheets/{id}?full=true, leaking metadata (name, currency, customer hierarchy) that would otherwise be filtered out by the team ACL.
Details
Entry point — only ownership is checked in src/API/TimesheetController.php:317-355
#[IsGranted('edit', 'timesheet')]
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_timesheet', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Timesheet $timesheet): Response
{
...
$form = $this->createForm(TimesheetApiEditForm::class, $timesheet, [...]);
$form->setData($timesheet);
$form->submit($request->request->all(), false);
if (false === $form->isValid()) { ... }
$this->service->saveTimesheet($timesheet);
...
}
src/Voter/TimesheetVoter.php:134-142:
if ($subject->getUser()?->getId() === $user->getId()) {
return $this->permissionManager->hasRolePermission($user, $permission . '_own_timesheet');
}
if (!$this->permissionManager->checkTeamAccessTimesheet($subject, $user)) {
return false;
}
For an own-timesheet, only edit_own_timesheet is required. The voter does not look at the new project being submitted; it only validates the existing record's ownership.
Form replays user-controlled project ID into the access query
src/Form/TimesheetEditForm.php:60-71:
$isNew = true;
if (isset($options['data']) && $options['data'] instanceof Timesheet) {
...
if (null !== $entry->getId()) {
$isNew = false;
}
...
}
$this->addProject($builder, $isNew, $project, $customer);
src/Form/FormTrait.php:59-100:
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($builder, $project, $customer, $isNew, $options): void {
$data = $event->getData();
$customer = \array_key_exists('customer', $data) && $data['customer'] !== '' ? $data['customer'] : null;
$project = \array_key_exists('project', $data) && $data['project'] !== '' ? $data['project'] : $project;
$event->getForm()->add('project', ProjectType::class, array_merge($options, [
'group_by' => null,
'query_builder' => function (ProjectRepository $repo) use ($builder, $project, $customer, $isNew) {
$project = \is_string($project) ? (int) $project : $project;
...
if ($isNew && \is_int($project)) {
$project = $repo->find($project);
if ($project !== null) {
if (!$project->getCustomer()->isVisible()) { ... $project = null; }
elseif (!$project->isVisible()) { $project = null; }
}
}
...
$query = new ProjectFormTypeQuery($project, $customer);
$query->setUser($builder->getOption('user'));
$query->setWithCustomer(true);
return $repo->getQueryBuilderForFormType($query);
},
]));
}
);
Two problems compound:
- The visibility re-check on line 73 is gated on
$isNew. For PATCH,$isNew = false, so the closure passes the attacker-supplied ID straight through. - Even when
$isNew = true(POST), the re-check only validatesisVisible()— it does not validate team membership.
The query-builder unconditionally accepts the submitted ID
…
- Record
GHSA-vrr2-g9gh-c3jc- CVSS 4.0
- 5.3 Medium
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-639
- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
kimai/kimaiPackagist |
through 2.56.0 | 2.57.0 |
kimai/kimaiPackagist |
before 2.57.0, plus listed versions 0.10.20.30.40.5 Show 45 moreShow less0.60.6.10.70.80.8.10.91.01.0.11.11.101.10.11.10.21.111.11.11.121.131.141.14.11.14.21.14.31.151.15.11.15.21.15.31.15.41.15.51.15.61.161.16.11.16.101.16.21.16.31.16.41.16.51.16.61.16.71.16.81.16.91.171.17.11.181.18.11.18.21.191.19.1 |
2.57.0 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-52820 is a medium vulnerability (CVSS 5.3) in Kimai, published July 13, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.45%, the 39th percentile. Fixed in kimai/kimai 2.57.0.
References
github.com/kimai/kimai/security/advisories/GHSA-vrr2-g9gh-c3jc
github.com/kimai/kimai/pull/5929
github.com/kimai/kimai/commit/976d38e8a4485a1c923ee7b5841849e91a06e849
github.com/kimai/kimai/releases/tag/2.57.0
kimai.org/en/security/ghsa-vrr2-g9gh-c3jc
github.com/advisories/GHSA-vrr2-g9gh-c3jc
github.com/CVEProject/cvelistV5/tree/main/cves/2026/52xxx/CVE-2026-52820.json
nvd.nist.gov/vuln/detail/CVE-2026-52820
github.com/kimai/kimai
Left out here: 152 versions and the rest of 2 source descriptions. The JSON record has them.