Kimai's User Preferences API allows standard users to modify restricted attributes: hourly_rate, internal_rate
The advisory, in context
Summary
A Mass Assignment / Broken Object Property Level Authorization (BOPA) vulnerability in the User Preferences API allows any authenticated user (even those with the lowest privileges) to arbitrarily modify restricted financial attributes on their profile, specifically their hourly_rate and internal_rate.
Details
Kimai restrictively protects the hourly_rate and internal_rate parameters during standard GUI flow. Users lacking the hourly-rate role permissions cannot see or edit these fields via the standard Web Form (UserApiEditForm / UserEditType).
The vulnerability exists in the dedicated preferences API endpoint: src/API/UserController.php::updateUserPreference.
When a PATCH request is sent to /api/users/{id}/preferences, the endpoint iterates through the submitted JSON array and blindly applies the new values:
foreach ($request->request->all() as $preference) {
// ... validation omitted ...
if (null === ($meta = $profile->getPreference($name))) {
throw $this->createNotFoundException(\sprintf('Unknown custom-field "%s" requested', $name));
}
$meta->setValue($value); // <-- VULNERABILITY
}
The underlying Role-Based Access Control logic (UserPreferenceSubscriber::getDefaultPreferences) accurately identifies that standard users lack the hourly-rate role, and flags the dynamically generated preference object as disabled ($preference->setEnabled(false)).
However, the updateUserPreference API endpoint entirely ignores this isEnabled() flag and forcefully saves the mutated object to the database natively via Doctrine ORM. This allows unauthorized accounts to manipulate the business-logic variables calculating their own financial earnings.
PoC
- Log into Kimai as an unprivileged, standard employee account (a user with absolutely no
rolesarray privileges). - Capture the
cookieor Session cookies. (In this example, the user's ID is2). - Send the following cURL request (or intercept via Burp Suite) targeting your own user ID:
curl -i -X PATCH "http://localhost:8001/api/users/2/preferences" \
-H "Content-Type: application/json" \
-H "cookie: <YOUR_STANDARD_USER_TOKEN>" \
-d '[
{
"name": "hourly_rate",
"value": "1337"
},
{
"name": "internal_rate",
"value": "1337"
}
]'
- The server responds with
HTTP/1.1 200 OK. (Note: Thehourly_ratewill intentionally NOT appear in the JSON echo due toUser::getVisiblePreferencessanitizing output based on the same disabled flag). - If an Administrator organically views User 2's profile within Kimai, or if the user logs any new timesheets, the active and billed
hourly_rateapplied to their account will be confirmed as1337. <img width="1542" height="1039" alt="user_account" src="https://github.com/user-attachments/assets/fff5e2da-d598-408d-8a01-784499ade844" /> <img width="1539" height="1037" alt="admin_account" src="https://github.com/user-attachments/assets/86a6e8c3-a97f-4be3-9f9f-2e23fad1d8a0" />
Impact
This is a Privilege Escalation and Business Logic Flaw impacting the core financial calculations of the application. An attacker with a standard user account can manipulate their own billing rate multipliers unbeknownst to administrators, resulting in fraudulent invoices, distorted timesheet exports, and unauthorized financial tampering.
Affected packages
Start with the recorded fix for your release branch. Check your installed version and test compatibility before upgrading. Fixing this advisory does not rule out other vulnerabilities in the release.
Earlier releases before 2.53.0
Fixed in 2.53.0
198 explicitly affected versions
0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.6.1, 0.7, 0.8, 0.8.1, 0.9, 1.0, 1.0.1, 1.1, 1.10, 1.10.1, 1.10.2, 1.11, 1.11.1, 1.12, 1.13, 1.14, 1.14.1, 1.14.2, 1.14.3, 1.15, 1.15.1, 1.15.2, 1.15.3, 1.15.4, 1.15.5, 1.15.6, 1.16, 1.16.1, 1.16.10, 1.16.2, 1.16.3, 1.16.4, 1.16.5, 1.16.6, 1.16.7, 1.16.8, 1.16.9, 1.17, 1.17.1, 1.18, 1.18.1, 1.18.2, 1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4, 1.19.5, 1.19.6, 1.19.7, 1.2, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.21.0, 1.22.0, 1.22.1, 1.23.0, 1.23.1, 1.24.0, 1.25.0, 1.26.0, 1.27.0, 1.28.0, 1.28.1, 1.29.0, 1.29.1, 1.3, 1.30.0, 1.30.1, 1.30.10, 1.30.11, 1.30.2, 1.30.3, 1.30.4, 1.30.5, 1.30.6, 1.30.7, 1.30.8, 1.30.9, 1.4, 1.4.1, 1.4.2, 1.5, 1.6, 1.6.1, 1.6.2, 1.7, 1.8, 1.9, 2.0.0, 2.0.0-alpha, 2.0.0-beta, 2.0.0-beta-2, 2.0.0-beta-3, 2.0.0-rc-1, 2.0.1, 2.0.10, 2.0.11, 2.0.12, 2.0.13, 2.0.14, 2.0.15, 2.0.16, 2.0.17, 2.0.18, 2.0.19, 2.0.2, 2.0.20, 2.0.21, 2.0.22, 2.0.23, 2.0.24, 2.0.25, 2.0.26, 2.0.27, 2.0.28, 2.0.29, 2.0.3, 2.0.30, 2.0.31, 2.0.32, 2.0.33, 2.0.34, 2.0.35, 2.0.4, 2.0.5, 2.0.6, 2.0.7, 2.0.8, 2.0.9, 2.1.0, 2.10.0, 2.11.0, 2.12.0, 2.13.0, 2.14.0, 2.15.0, 2.16.0, 2.16.1, 2.17.0, 2.18.0, 2.19.0, 2.19.1, 2.2.0, 2.2.1, 2.20.0, 2.20.1, 2.21.0, 2.22.0, 2.23.0, 2.24.0, 2.25.0, 2.26.0, 2.27.0, 2.28.0, 2.29.0, 2.3.0, 2.30.0, 2.31.0, 2.32.0, 2.33.0, 2.34.0, 2.35.0, 2.35.1, 2.36.0, 2.36.1, 2.37.0, 2.38.0, 2.39.0, 2.4.0, 2.4.1, 2.40.0, 2.41.0, 2.42.0, 2.43.0, 2.44.0, 2.45.0, 2.46.0, 2.47.0, 2.48.0, 2.49.0, 2.5.0, 2.50.0, 2.51.0, 2.52.0, 2.6.0, 2.7.0, 2.8.0, 2.9.0
View affected-version evidence
{
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.53.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.1",
"0.2",
"0.3",
"0.4",
"0.5",
"0.6",
"0.6.1",
"0.7",
"0.8",
"0.8.1",
"0.9",
"1.0",
"1.0.1",
"1.1",
"1.10",
"1.10.1",
"1.10.2",
"1.11",
"1.11.1",
"1.12",
"1.13",
"1.14",
"1.14.1",
"1.14.2",
"1.14.3",
"1.15",
"1.15.1",
"1.15.2",
"1.15.3",
"1.15.4",
"1.15.5",
"1.15.6",
"1.16",
"1.16.1",
"1.16.10",
"1.16.2",
"1.16.3",
"1.16.4",
"1.16.5",
"1.16.6",
"1.16.7",
"1.16.8",
"1.16.9",
"1.17",
"1.17.1",
"1.18",
"1.18.1",
"1.18.2",
"1.19",
"1.19.1",
"1.19.2",
"1.19.3",
"1.19.4",
"1.19.5",
"1.19.6",
"1.19.7",
"1.2",
"1.20",
"1.20.1",
"1.20.2",
"1.20.3",
"1.20.4",
"1.21.0",
"1.22.0",
"1.22.1",
"1.23.0",
"1.23.1",
"1.24.0",
"1.25.0",
"1.26.0",
"1.27.0",
"1.28.0",
"1.28.1",
"1.29.0",
"1.29.1",
"1.3",
"1.30.0",
"1.30.1",
"1.30.10",
"1.30.11",
"1.30.2",
"1.30.3",
"1.30.4",
"1.30.5",
"1.30.6",
"1.30.7",
"1.30.8",
"1.30.9",
"1.4",
"1.4.1",
"1.4.2",
"1.5",
"1.6",
"1.6.1",
"1.6.2",
"1.7",
"1.8",
"1.9",
"2.0.0",
"2.0.0-alpha",
"2.0.0-beta",
"2.0.0-beta-2",
"2.0.0-beta-3",
"2.0.0-rc-1",
"2.0.1",
"2.0.10",
"2.0.11",
"2.0.12",
"2.0.13",
"2.0.14",
"2.0.15",
"2.0.16",
"2.0.17",
"2.0.18",
"2.0.19",
"2.0.2",
"2.0.20",
"2.0.21",
"2.0.22",
"2.0.23",
"2.0.24",
"2.0.25",
"2.0.26",
"2.0.27",
"2.0.28",
"2.0.29",
"2.0.3",
"2.0.30",
"2.0.31",
"2.0.32",
"2.0.33",
"2.0.34",
"2.0.35",
"2.0.4",
"2.0.5",
"2.0.6",
"2.0.7",
"2.0.8",
"2.0.9",
"2.1.0",
"2.10.0",
"2.11.0",
"2.12.0",
"2.13.0",
"2.14.0",
"2.15.0",
"2.16.0",
"2.16.1",
"2.17.0",
"2.18.0",
"2.19.0",
"2.19.1",
"2.2.0",
"2.2.1",
"2.20.0",
"2.20.1",
"2.21.0",
"2.22.0",
"2.23.0",
"2.24.0",
"2.25.0",
"2.26.0",
"2.27.0",
"2.28.0",
"2.29.0",
"2.3.0",
"2.30.0",
"2.31.0",
"2.32.0",
"2.33.0",
"2.34.0",
"2.35.0",
"2.35.1",
"2.36.0",
"2.36.1",
"2.37.0",
"2.38.0",
"2.39.0",
"2.4.0",
"2.4.1",
"2.40.0",
"2.41.0",
"2.42.0",
"2.43.0",
"2.44.0",
"2.45.0",
"2.46.0",
"2.47.0",
"2.48.0",
"2.49.0",
"2.5.0",
"2.50.0",
"2.51.0",
"2.52.0",
"2.6.0",
"2.7.0",
"2.8.0",
"2.9.0"
]
}