CVE-2026-10215
Description
Dolibarr ERP CRM up to 23.0.1 contains an improper authorization vulnerability in the Leave Request REST API, allowing bypass of access controls.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Dolibarr ERP CRM up to 23.0.1 contains an improper authorization vulnerability in the Leave Request REST API, allowing bypass of access controls.
Vulnerability
Dolibarr ERP CRM up to version 23.0.1 includes an improper authorization vulnerability in the Leave Request REST API. The function checkUserAccessToObject in the file htdocs/holiday/class/api_holidays.class.php fails to properly validate user permissions for leave request objects. The issue arises because the API passes only the resource ID to the access check, rather than the full resource object, allowing the hierarchy-based access control to be bypassed. The vulnerability is present in versions 23.0.0 and earlier, confirmed up to 23.0.1 [1][3][4].
Exploitation
An attacker requires a valid account with low privileges (e.g., an internal user who can read only their own and their subordinates' leave requests). The attack is performed remotely through HTTP requests to the Leave Request API endpoints (e.g., GET, POST, PUT for leave requests). By crafting API calls that include a leave request ID belonging to another ordinary user, the attacker can retrieve or manipulate data without proper authorization. The exploit does not require any special network position beyond normal API access [3][4].
Impact
Successful exploitation allows an attacker to read leave request data belonging to other users within the same entity, bypassing the intended access restrictions. This results in unauthorized information disclosure (confidentiality impact). The attacker cannot gain code execution or elevate privileges beyond reading others' leave data, but the exposure of employee absence patterns could lead to further privacy concerns [3][4].
Mitigation
Dolibarr has released version 23.0.2 which includes the fix (commit ee93b6f2f9dd0f6aeefe9d718ab3ab0a44326b73) [1][2]. Users should upgrade to 23.0.2 or later. No workarounds have been published for unpatched versions. The vulnerability is not currently listed on the CISA KEV.
AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
1ee93b6f2f9ddFIX #GHSA-qjj8-wpvx-p54j - test on hierarchy not done on some api
3 files changed · +36 −36
htdocs/api/class/api.class.php+7 −7 modified@@ -382,13 +382,13 @@ protected function _cleanObjectDatas($object) /** * Check access by user to a given resource * - * @param string $resource element to check - * @param int $resource_id Object ID if we want to check a particular record (optional) is linked to a owned thirdparty (optional). - * @param string $dbtablename 'TableName&SharedElement' with Tablename is table where object is stored. SharedElement is an optional key to define where to check entity. Not used if objectid is null (optional) - * @param string $feature2 Feature to check, second level of permission (optional). Can be or check with 'level1|level2'. - * @param string $dbt_keyfield Field name for socid foreign key if not fk_soc. Not used if objectid is null (optional) - * @param string $dbt_select Field name for select if not rowid. Not used if objectid is null (optional) - * @return bool + * @param string $resource element to check + * @param int|string|Object $resource_id Full object or object ID or list of object id. For example if we want to check a particular record (optional) is linked to a owned thirdparty (optional). + * @param string $dbtablename 'TableName&SharedElement' with Tablename is table where object is stored. SharedElement is an optional key to define where to check entity. Not used if objectid is null (optional) + * @param string $feature2 Feature to check, second level of permission (optional). Can be or check with 'level1|level2'. + * @param string $dbt_keyfield Field name for socid foreign key if not fk_soc. Not used if objectid is null (optional) + * @param string $dbt_select Field name for select if not rowid. Not used if objectid is null (optional) + * @return bool */ protected static function _checkAccessToResource($resource, $resource_id = 0, $dbtablename = '', $feature2 = '', $dbt_keyfield = 'fk_soc', $dbt_select = 'rowid') {
htdocs/expensereport/class/api_expensereports.class.php+17 −17 modified@@ -106,7 +106,7 @@ public function get($id) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -281,10 +281,10 @@ public function getLines($id) $result = $this->expensereport->fetch($id); if (!$result) { - throw new RestException(404, 'expensereport not found'); + throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } $this->expensereport->fetch_lines(); @@ -324,7 +324,7 @@ public function postLine($id, $request_data = null) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -383,10 +383,10 @@ public function putLine($id, $lineid, $request_data = null) $result = $this->expensereport->fetch($id); if (!$result) { - throw new RestException(404, 'expensereport not found'); + throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -454,7 +454,7 @@ public function deleteLine($id, $lineid) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -509,10 +509,10 @@ public function put($id, $request_data = null) $result = $this->expensereport->fetch($id); if (!$result) { - throw new RestException(404, 'expensereport not found'); + throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } foreach ($request_data as $field => $value) { @@ -562,10 +562,10 @@ public function delete($id) $result = $this->expensereport->fetch($id); if (!$result) { - throw new RestException(404, 'Expense Report not found'); + throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -606,7 +606,7 @@ public function setToDraft($id) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -650,7 +650,7 @@ public function validate($id, $notrigger = 0) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -695,7 +695,7 @@ public function approve($id, $notrigger = 0) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -741,7 +741,7 @@ public function deny($id, $details, $notrigger = 0) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -787,7 +787,7 @@ public function setPaid($id, $notrigger = 0) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -829,7 +829,7 @@ public function cancel($id, $detail, $notrigger = 0) throw new RestException(404, 'Expense report not found'); } - if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport->id)) { + if (!DolibarrApi::_checkAccessToResource('expensereport', $this->expensereport)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); }
htdocs/holiday/class/api_holidays.class.php+12 −12 modified@@ -84,7 +84,7 @@ public function get($id) throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -262,10 +262,10 @@ public function put($id, $request_data = null) $result = $this->holiday->fetch($id); if (!$result) { - throw new RestException(404, 'holiday not found'); + throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } foreach ($request_data as $field => $value) { @@ -318,7 +318,7 @@ public function delete($id) throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -363,7 +363,7 @@ public function validate($id, $notrigger = 0) throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -409,7 +409,7 @@ public function approve($id, $notrigger = 0) throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -452,10 +452,10 @@ public function cancel($id, $notrigger = 0) $result = $this->holiday->fetch($id); if (!$result) { - throw new RestException(404, 'Holiday not found'); + throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -499,10 +499,10 @@ public function refuse($id, $detail_refuse, $notrigger = 0) $result = $this->holiday->fetch($id); if (!$result) { - throw new RestException(404, 'Holiday not found'); + throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); } @@ -549,10 +549,10 @@ public function reopen($id, $notrigger = 0) $result = $this->holiday->fetch($id); if (!$result) { - throw new RestException(404, 'Holiday not found'); + throw new RestException(404, 'Leave not found'); } - if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)) { + if (!DolibarrApi::_checkAccessToResource('holiday', $this->holiday)) { throw new RestException(403, 'Access not allowed for login '.DolibarrApiAccess::$user->login); }
Vulnerability mechanics
Root cause
"The authorization check passed only a numeric object ID instead of the full object, so the hierarchical ownership test was never performed."
Attack vector
An authenticated attacker with low privileges can call any Leave Request REST API endpoint (e.g. `get`, `validate`, `approve`, `cancel`) and pass the ID of a leave request belonging to another user. The old code passed only the numeric ID (`$this->holiday->id`) to `_checkAccessToResource`, which skipped the hierarchical ownership check. The attacker can therefore read or modify another user's leave requests remotely [ref_id=1]. The CVSS vector confirms the attack is over the network with low complexity and requires only low privileges [CWE-285].
Affected code
The vulnerability is in the function `checkUserAccessToObject` (called via `DolibarrApi::_checkAccessToResource`) in `htdocs/holiday/class/api_holidays.class.php` and `htdocs/expensereport/class/api_expensereports.class.php`. The patch also updates the signature of `_checkAccessToResource` in `htdocs/api/class/api.class.php` to accept an object instead of just an integer ID [patch_id=4023023].
What the fix does
The patch changes every call from `DolibarrApi::_checkAccessToResource('holiday', $this->holiday->id)` to `DolibarrApi::_checkAccessToResource('holiday', $this->holiday)`, passing the full holiday object instead of just its ID. The same change is applied to the expense report API. The `_checkAccessToResource` method signature in `api.class.php` is updated to accept `int|string|Object` for the `$resource_id` parameter, enabling it to inspect the object's properties (such as the user/owner hierarchy) that were previously ignored when only a scalar ID was provided [patch_id=4023023]. The commit message confirms the fix addresses "test on hierarchy not done on some api" [ref_id=1].
Preconditions
- authAttacker must have a valid authenticated session with low-privilege user account
- configTarget Dolibarr instance must have the Leave Request REST API enabled
- inputAttacker must know or enumerate the ID of another user's leave request
Generated on Jun 1, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/Dolibarr/dolibarr/commit/ee93b6f2f9dd0f6aeefe9d718ab3ab0a44326b73nvd
- github.com/Dolibarr/dolibarr/issues/37752nvd
- github.com/Dolibarr/dolibarr/issues/37752nvd
- github.com/Dolibarr/dolibarr/releases/tag/23.0.2nvd
- vuldb.com/cve/CVE-2026-10215nvd
- vuldb.com/submit/821930nvd
- vuldb.com/vuln/367494nvd
- vuldb.com/vuln/367494/ctinvd
News mentions
1- Assimp: Five Memory-Safety Bugs Disclosed in glTF and FBX ParsersVypr Intelligence · Jun 1, 2026