Improper Authorization in dolibarr/dolibarr
Description
An Improper Authorization vulnerability exists in Dolibarr versions prior to the 'develop' branch. A user with restricted permissions in the 'Reception' section is able to access specific reception details via direct URL access, bypassing the intended permission restrictions.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Dolibarr ERP/CRM prior to the develop branch contains an improper authorization vulnerability allowing restricted users to access reception details via direct URL.
CVE-2021-3991 is an improper authorization vulnerability in Dolibarr ERP/CRM, an open-source business management suite [1]. The flaw resides in the Reception module: a user with restricted permissions (e.g., lacking 'lire' or 'read' rights) can bypass intended access controls by directly navigating to reception detail URLs [2][3]. The root cause is missing permission checks in the receptioncard.php script, which fails to enforce the restrictedArea() function for all access paths [2].
Exploitation requires an authenticated session with a role that has limited Reception permissions. No special network position is needed; the attacker simply crafts a direct URL to a reception record (e.g., /reception/card.php?id=X). The vulnerability was introduced because the permission check was only applied when the origin was 'reception', leaving other entry points unguarded [2].
A successful attacker can view sensitive reception details, such as supplier order information, stock movements, and associated documents, that should be hidden from their role [3][4]. This could lead to information disclosure and potential business intelligence leakage, though no data modification or privilege escalation is reported.
The issue was fixed in the 'develop' branch via commit 63cd063, which restructures the permission logic to always verify reception rights when the module is enabled [2]. Users are advised to update to the latest develop version or apply the patch manually. No official workaround is documented, and the vulnerability is not listed on CISA's Known Exploited Vulnerabilities catalog.
- GitHub - Dolibarr/dolibarr: Dolibarr ERP CRM is a modern software package to manage your company or foundation's activity (contacts, suppliers, invoices, orders, stocks, agenda, accounting, ...). it's an open source Web application (written in PHP) designed for businesses of any sizes, foundations and freelancers.
- Debug permission on supplier order. · Dolibarr/dolibarr@63cd063
- NVD - CVE-2021-3991
- The world’s first bug bounty platform for AI/ML
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
dolibarr/dolibarrPackagist | < 15.0.0 | 15.0.0 |
Affected products
3- osv-coords2 versions
< 20.0.2+ 1 more
- (no CPE)range: < 20.0.2
- (no CPE)range: < 15.0.0
- dolibarr/dolibarr/dolibarrv5Range: unspecified
Patches
163cd06394f39Debug permission on supplier order.
8 files changed · +101 −47
htdocs/core/menus/standard/eldy.lib.php+2 −1 modified@@ -2149,7 +2149,8 @@ function print_left_eldy_menu($db, $menu_array_before, $menu_array_after, &$tabM // Not enabled but visible (so greyed), except if parent was not enabled. print '<div class="menu_contenu'.$cssmenu.'">'; print $tabstring; - print '<span class="vsmenudisabled vsmenudisabledmargin">'.$menu_array[$i]['titre'].'</span><br></div>'."\n"; + print '<span class="vsmenudisabled vsmenudisabledmargin">'.$menu_array[$i]['titre'].'</span><br>'; + print '</div>'."\n"; } }
htdocs/fourn/class/fournisseur.commande.class.php+8 −1 modified@@ -2295,7 +2295,14 @@ public function Livraison($user, $date, $type, $comment) dol_syslog(get_class($this)."::Livraison"); - if ($user->rights->fournisseur->commande->receptionner) { + $usercanreceive = 0; + if (empty($conf->reception->enabled)) { + $usercanreceive = $user->rights->fournisseur->commande->receptionner; + } else { + $usercanreceive = $user->rights->reception->creer; + } + + if ($usercanreceive) { // Define the new status if ($type == 'par') { $statut = self::STATUS_RECEIVED_PARTIALLY;
htdocs/fourn/commande/card.php+7 −2 modified@@ -90,7 +90,6 @@ if ($user->socid) { $socid = $user->socid; } -$result = restrictedArea($user, 'fournisseur', $id, 'commande_fournisseur', 'commande'); // Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context $hookmanager->initHooks(array('ordersuppliercard', 'globalcard')); @@ -124,6 +123,8 @@ } } +$result = restrictedArea($user, 'fournisseur', $id, 'commande_fournisseur', 'commande'); + // Common permissions $usercanread = ($user->rights->fournisseur->commande->lire || $user->rights->supplier_order->lire); $usercancreate = ($user->rights->fournisseur->commande->creer || $user->rights->supplier_order->creer); @@ -136,7 +137,11 @@ $usercanapprove = $user->rights->fournisseur->commande->approuver; $usercanapprovesecond = $user->rights->fournisseur->commande->approve2; $usercanorder = $user->rights->fournisseur->commande->commander; -$usercanreceived = $user->rights->fournisseur->commande->receptionner; +if (empty($conf->reception->enabled)) { + $usercanreceive = $user->rights->fournisseur->commande->receptionner; +} else { + $usercanreceive = $user->rights->reception->creer; +} // Permissions for includes $permissionnote = $usercancreate; // Used by the include of actions_setnotes.inc.php
htdocs/fourn/commande/dispatch.php+34 −15 modified@@ -62,11 +62,6 @@ if ($user->socid) { $socid = $user->socid; } -$result = restrictedArea($user, 'fournisseur', $id, 'commande_fournisseur', 'commande'); - -if (empty($conf->stock->enabled)) { - accessforbidden(); -} $hookmanager->initHooks(array('ordersupplierdispatch')); @@ -89,6 +84,21 @@ } } +if (empty($conf->reception->enabled)) { + $permissiontoreceive = $user->rights->fournisseur->commande->receptionner; + $permissiontocontrol = ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->fournisseur->commande->receptionner)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->fournisseur->commande_advance->check))); +} else { + $permissiontoreceive = $user->rights->reception->creer; + $permissiontocontrol = ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->reception->creer)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->reception->reception_advance->validate))); +} + +// $id is id of a purchase order. +$result = restrictedArea($user, 'fournisseur', $id, 'commande_fournisseur', 'commande'); + +if (empty($conf->stock->enabled)) { + accessforbidden(); +} + /* * Actions @@ -100,7 +110,7 @@ setEventMessages($hookmanager->error, $hookmanager->errors, 'errors'); } -if ($action == 'checkdispatchline' && !((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande->receptionner)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande_advance->check)))) { +if ($action == 'checkdispatchline' && $permissiontocontrol) { $error = 0; $supplierorderdispatch = new CommandeFournisseurDispatch($db); @@ -137,7 +147,7 @@ } } -if ($action == 'uncheckdispatchline' && !((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande->receptionner)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande_advance->check)))) { +if ($action == 'uncheckdispatchline' && $permissiontocontrol) { $error = 0; $supplierorderdispatch = new CommandeFournisseurDispatch($db); @@ -173,7 +183,7 @@ } } -if ($action == 'denydispatchline' && !((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande->receptionner)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande_advance->check)))) { +if ($action == 'denydispatchline' && $permissiontocontrol) { $error = 0; $supplierorderdispatch = new CommandeFournisseurDispatch($db); @@ -209,7 +219,7 @@ } } -if ($action == 'dispatch' && $user->rights->fournisseur->commande->receptionner) { +if ($action == 'dispatch' && $permissiontoreceive) { $error = 0; $db->begin(); @@ -387,7 +397,7 @@ } // Remove a dispatched line -if ($action == 'confirm_deleteline' && $confirm == 'yes' && $user->rights->fournisseur->commande->receptionner) { +if ($action == 'confirm_deleteline' && $confirm == 'yes' && $permissiontoreceive) { $db->begin(); $supplierorderdispatch = new CommandeFournisseurDispatch($db); @@ -430,7 +440,7 @@ } // Update a dispatched line -if ($action == 'updateline' && $user->rights->fournisseur->commande->receptionner) { +if ($action == 'updateline' && $permissiontoreceive) { $db->begin(); $error = 0; @@ -751,9 +761,9 @@ // Select warehouse to force it everywhere if (count($listwarehouses) > 1) { - print '<br>'.$langs->trans("ForceTo").' '.$form->selectarray('fk_default_warehouse', $listwarehouses, $fk_default_warehouse, 1, 0, 0, '', 0, 0, $disabled, '', 'minwidth100 maxwidth300', 1); + print '<br><span class="opacitymedium">'.$langs->trans("ForceTo").'</span> '.$form->selectarray('fk_default_warehouse', $listwarehouses, $fk_default_warehouse, 1, 0, 0, '', 0, 0, $disabled, '', 'minwidth100 maxwidth300', 1); } elseif (count($listwarehouses) == 1) { - print '<br>'.$langs->trans("ForceTo").' '.$form->selectarray('fk_default_warehouse', $listwarehouses, $fk_default_warehouse, 0, 0, 0, '', 0, 0, $disabled, '', 'minwidth100 maxwidth300', 1); + print '<br><span class="opacitymedium">'.$langs->trans("ForceTo").'</span> '.$form->selectarray('fk_default_warehouse', $listwarehouses, $fk_default_warehouse, 0, 0, 0, '', 0, 0, $disabled, '', 'minwidth100 maxwidth300', 1); } print '</td>'; @@ -1055,10 +1065,19 @@ $dispatchBt = empty($conf->reception->enabled) ? $langs->trans("Receive") : $langs->trans("CreateReception"); - print '<br><input type="submit" class="button" name="dispatch" value="'.dol_escape_htmltag($dispatchBt).'"'; + print '<br>'; + print '<input type="submit" class="button" name="dispatch" value="'.dol_escape_htmltag($dispatchBt).'"'; + $disabled = 0; + if (!$permissiontoreceive) { + $disabled = 1; + } if (count($listwarehouses) <= 0) { + $disabled = 1; + } + if ($disabled) { print ' disabled'; } + print '>'; } print '</div>'; @@ -1257,7 +1276,7 @@ // Add button to check/uncheck disaptching print '<td class="center">'; - if ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande->receptionner)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && empty($user->rights->fournisseur->commande_advance->check))) { + if (!$permissiontocontrol) { if (empty($objp->status)) { print '<a class="button buttonRefused" href="#">'.$langs->trans("Approve").'</a>'; print '<a class="button buttonRefused" href="#">'.$langs->trans("Deny").'</a>';
htdocs/reception/card.php+33 −24 modified@@ -112,9 +112,6 @@ // Initialize technical object to manage hooks of page. Note that conf->hooks_modules contains array of hook context $hookmanager->initHooks(array('receptioncard', 'globalcard')); -$permissiondellink = $user->rights->reception->creer; // Used by the include of actions_dellink.inc.php -//var_dump($object->lines[0]->detail_batch); - $date_delivery = dol_mktime(GETPOST('date_deliveryhour', 'int'), GETPOST('date_deliverymin', 'int'), 0, GETPOST('date_deliverymonth', 'int'), GETPOST('date_deliveryday', 'int'), GETPOST('date_deliveryyear', 'int')); if ($id > 0 || !empty($ref)) { @@ -142,16 +139,31 @@ $socid = $user->socid; } -if ($origin == 'reception') { +if (!empty($conf->reception->enabled) || $origin == 'reception' || empty($origin)) { $result = restrictedArea($user, 'reception', $id); } else { + // We do not use the reception module, so we test permission on the supplier orders if ($origin == 'supplierorder' || $origin == 'order_supplier') { $result = restrictedArea($user, 'fournisseur', $origin_id, 'commande_fournisseur', 'commande'); } elseif (empty($user->rights->{$origin}->lire) && empty($user->rights->{$origin}->read)) { accessforbidden(); } } +if (!empty($conf->reception->enabled)) { + $permissiontoread = $user->rights->reception->lire; + $permissiontoadd = $user->rights->reception->creer; + $permissiondellink = $user->rights->reception->creer; // Used by the include of actions_dellink.inc.php + $permissiontovalidate = ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->reception->creer)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->reception->reception_advance->validate))); + $permissiontodelete = $user->rights->reception->supprimer; +} else { + $permissiontoread = $user->rights->fournisseur->commande->receptionner; + $permissiontoadd = $user->rights->fournisseur->commande->receptionner; + $permissiondellink = $user->rights->fournisseur->commande->receptionner; // Used by the include of actions_dellink.inc.php + $permissiontovalidate = ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->fournisseur->commande->receptionner)) || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->fournisseur->commande_advance->check))); + $permissiontodelete = $user->rights->fournisseur->commande->receptionner; +} + /* * Actions @@ -171,12 +183,12 @@ include DOL_DOCUMENT_ROOT.'/core/actions_dellink.inc.php'; // Must be include, not include_once // Reopen - if ($action == 'reopen' && $user->rights->reception->creer) { + if ($action == 'reopen' && $permissiontoadd) { $result = $object->reOpen(); } // Confirm back to draft status - if ($action == 'modif' && $user->rights->reception->creer) { + if ($action == 'modif' && $permissiontoadd) { $result = $object->setDraft($user); if ($result >= 0) { // Define output language @@ -201,11 +213,11 @@ } // Set incoterm - if ($action == 'set_incoterms' && !empty($conf->incoterm->enabled)) { + if ($action == 'set_incoterms' && !empty($conf->incoterm->enabled) && $permissiontoadd) { $result = $object->setIncoterms(GETPOST('incoterm_id', 'int'), GETPOST('location_incoterms', 'alpha')); } - if ($action == 'setref_supplier') { + if ($action == 'setref_supplier' && $permissiontoadd) { if ($result < 0) { setEventMessages($object->error, $object->errors, 'errors'); } @@ -220,7 +232,7 @@ } } - if ($action == 'update_extras') { + if ($action == 'update_extras' && $permissiontoadd) { $object->oldcopy = dol_clone($object); // Fill array 'array_options' with data from update form @@ -244,7 +256,7 @@ } // Create reception - if ($action == 'add' && $user->rights->reception->creer) { + if ($action == 'add' && $permissiontoadd) { $error = 0; $predef = ''; @@ -405,10 +417,7 @@ $_GET["commande_id"] = GETPOST('commande_id', 'int'); $action = 'create'; } - } elseif ($action == 'confirm_valid' && $confirm == 'yes' && - ((empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->reception->creer)) - || (!empty($conf->global->MAIN_USE_ADVANCED_PERMS) && !empty($user->rights->reception->reception_advance->validate))) - ) { + } elseif ($action == 'confirm_valid' && $confirm == 'yes' && $permissiontovalidate) { $object->fetch_thirdparty(); $result = $object->valid($user); @@ -440,7 +449,7 @@ } } } - } elseif ($action == 'confirm_delete' && $confirm == 'yes' && $user->rights->reception->supprimer) { + } elseif ($action == 'confirm_delete' && $confirm == 'yes' && $permissiontodelete) { $result = $object->delete($user); if ($result > 0) { header("Location: ".DOL_URL_ROOT.'/reception/index.php'); @@ -455,7 +464,7 @@ if ($result < 0) { setEventMessages($object->error, $object->errors, 'errors'); }*/ - } elseif ($action == 'setdate_livraison' && $user->rights->reception->creer) { + } elseif ($action == 'setdate_livraison' && $permissiontoadd) { //print "x ".$_POST['liv_month'].", ".$_POST['liv_day'].", ".$_POST['liv_year']; $datedelivery = dol_mktime(GETPOST('liv_hour', 'int'), GETPOST('liv_min', 'int'), 0, GETPOST('liv_month', 'int'), GETPOST('liv_day', 'int'), GETPOST('liv_year', 'int')); @@ -506,7 +515,7 @@ } $action = ""; - } elseif ($action == 'builddoc') { + } elseif ($action == 'builddoc' && $permissiontoread) { // Build document // En get ou en post // Save last template used to generate document @@ -532,7 +541,7 @@ setEventMessages($object->error, $object->errors, 'errors'); $action = ''; } - } elseif ($action == 'remove_file') { + } elseif ($action == 'remove_file' && $permissiontoadd) { // Delete file in doc form require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; @@ -550,13 +559,13 @@ header('Location: '.$_SERVER["PHP_SELF"].'?id='.$object->id); exit(); } - } elseif ($action == 'classifyclosed') { + } elseif ($action == 'classifyclosed' && $permissiontoread) { $result = $object->setClosed(); if ($result >= 0) { header('Location: '.$_SERVER["PHP_SELF"].'?id='.$object->id); exit(); } - } elseif ($action == 'deleteline' && !empty($line_id)) { + } elseif ($action == 'deleteline' && !empty($line_id) && $permissiontoread) { // delete a line $lines = $object->lines; $line = new CommandeFournisseurDispatch($db); @@ -579,7 +588,7 @@ } else { setEventMessages($line->error, $line->errors, 'errors'); } - } elseif ($action == 'updateline' && $user->rights->reception->creer && GETPOST('save')) { + } elseif ($action == 'updateline' && GETPOST('save') && $permissiontoadd) { // Update a line // Clean parameters $qty = 0; @@ -666,11 +675,11 @@ $object->generateDocument($object->model_pdf, $outputlangs, $hidedetails, $hidedesc, $hideref); } } else { - header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id); // Pour reaffichage de la fiche en cours d'edition + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id); // To reshow the record we edit exit(); } - } elseif ($action == 'updateline' && $user->rights->reception->creer && GETPOST('cancel', 'alpha') == $langs->trans("Cancel")) { - header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id); // Pour reaffichage de la fiche en cours d'edition + } elseif ($action == 'updateline' && $permissiontoadd && GETPOST('cancel', 'alpha') == $langs->trans("Cancel")) { + header('Location: '.$_SERVER['PHP_SELF'].'?id='.$object->id); // To reshow the record we edit exit(); }
htdocs/theme/eldy/global.inc.php+2 −2 modified@@ -2881,12 +2881,12 @@ color: #202020; margin: 1px 1px 1px 6px; } -font.vsmenudisabled { font-family: <?php print $fontlist ?>; text-align: <?php print $left; ?>; color: #aaa; } +span.vsmenudisabled, font.vsmenudisabled { font-family: <?php print $fontlist ?>; text-align: <?php print $left; ?>; color: #aaa; } a.vsmenu:link, a.vsmenu:visited { color: var(--colortextbackvmenu); white-space: nowrap; } -font.vsmenudisabledmargin { margin: 1px 1px 1px 6px; } +span.vsmenudisabledmargin, font.vsmenudisabledmargin { margin: 1px 1px 1px 6px; } li a.vsmenudisabled, li.vsmenudisabled { color: #aaa !important; } a.help:link, a.help:visited, a.help:hover, a.help:active, span.help { text-align: <?php print $left; ?>; color: #aaa; text-decoration: none; }
htdocs/theme/md/style.css.php+2 −2 modified@@ -2928,9 +2928,9 @@ a.vmenu:link, a.vmenu:visited { color: #<?php echo $colortextbackvmenu; ?>; } a.vsmenu:link, a.vsmenu:visited, a.vsmenu:hover, a.vsmenu:active, span.vsmenu { font-size:<?php print $fontsize ?>px; font-family: <?php print $fontlist ?>; text-align: <?php print $left; ?>; font-weight: normal; color: #202020; margin: 1px 1px 1px 8px; } -font.vsmenudisabled { font-size:<?php print $fontsize ?>px; font-family: <?php print $fontlist ?>; text-align: <?php print $left; ?>; font-weight: normal; color: #aaa; } +span.vsmenudisabled, font.vsmenudisabled { font-size:<?php print $fontsize ?>px; font-family: <?php print $fontlist ?>; text-align: <?php print $left; ?>; font-weight: normal; color: #aaa; } a.vsmenu:link, a.vsmenu:visited { color: #<?php echo $colortextbackvmenu; ?>; white-space: nowrap; } -font.vsmenudisabledmargin { margin: 1px 1px 1px 8px; } +span.vsmenudisabledmargin, font.vsmenudisabledmargin { margin: 1px 1px 1px 8px; } a.help:link, a.help:visited, a.help:hover, a.help:active, span.help { text-align: <?php print $left; ?>; font-weight: normal; color: #999; text-decoration: none; }
htdocs/user/perms.php+13 −0 modified@@ -317,6 +317,19 @@ continue; } + // Special cases + if (!empty($conf->reception->enabled)) { + // The 2 permission in fournisseur modules has been replaced by permissions into reception module + if ($obj->module == 'fournisseur' && $obj->perms == 'commande' && $obj->subperms == 'receptionner') { + $i++; + continue; + } + if ($obj->module == 'fournisseur' && $obj->perms == 'commande_advance' && $obj->subperms == 'check') { + $i++; + continue; + } + } + $objMod = $modules[$obj->module]; // Save field module_position in database if value is wrong
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.