Every CVE Search/ Get API key

CVE-2026-52820

Kimai: Timesheet PATCH/POST allows assigning to project outside user's team via query_builder OR-bypass

5.3 MediumFix available

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

0.45%Up 0.1 points since Sep 15, 2026
90%50%10%1%

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 MediumCVSS: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 MediumCVSS: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:

  1. 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.
  2. Even when $isNew = true (POST), the re-check only validates isVisible() — it does not validate team membership.

The query-builder unconditionally accepts the submitted ID

…

Record
GHSA-vrr2-g9gh-c3jc
CVSS 4.0
5.3 MediumCVSS: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 MediumCVSS: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 MediumCVSS: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:

  1. 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.
  2. Even when $isNew = true (POST), the re-check only validates isVisible() — it does not validate team membership.

The query-builder unconditionally accepts the submitted ID

…

Record
GHSA-vrr2-g9gh-c3jc
CVSS 4.0
5.3 MediumCVSS: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

Affected packages
PackageAffectedFixed
kimai/kimai
Packagist
through 2.56.0
2.57.0
kimai/kimai
Packagist
before 2.57.0, plus listed versions
0.10.20.30.40.5
Show 45 moreShow less
0.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

History
RecordedChangedSources
first indexedCVE 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

This page doesn't list everything

Left out here: 152 versions and the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open