CodeAnt AI Security Research
CVE-2026-71503 Dolibarr XSS and Account Takeover

Amartya Jha
CEO, CodeAnt AI
TL;DR
CVE-2026-71503 is a reflected cross-site scripting bug in Dolibarr ERP CRM through 25.0.0-alpha, scored CVSS 9.3. A single link, opened once by a logged-in administrator, injects script into a shared extra-fields template. That script reads Dolibarr's non-rotating anti-CSRF token from a <meta> tag and, with no default Content-Security-Policy, submits the native user-creation form same-origin. The result is a second administrator account with an attacker-chosen password and REST API key. It is fixed in Dolibarr 24.0.0.
Cookie theft failed, and it turned out not to matter. The session cookie is httponly (htdocs/main.inc.php:141), so the payload's document.cookie came back empty. Stealing it was never the plan.
The anti-CSRF token does not rotate on every page load, and Dolibarr prints it into every page as <meta name="anti-csrf-newtoken">. No Content-Security-Policy is sent on a default install. So the injected script read the token out of its own DOM and posted it, same-origin, to the native user-creation form.
Three individually sensible decisions composed into a 9.3: a hardened cookie, a stable token exposed for the app's own AJAX, and no default CSP. Credit where it is due first. Dolibarr is maintained by volunteers with a small commercial sponsor and no funded security team, and they shipped a fix in 24.0.0 before this post existed.
What is CVE-2026-71503?
CVE-2026-71503 is a reflected cross-site scripting vulnerability in Dolibarr ERP CRM through 25.0.0-alpha, classified as CWE-79 and scored CVSS 9.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N). The type request parameter is echoed unescaped into a single-quoted JavaScript literal in the shared extra-fields template.
When a logged-in administrator opens a crafted URL, the injected script reads the non-rotating anti-CSRF token from the page's <meta name="anti-csrf-newtoken"> tag and, with no default Content-Security-Policy, submits the native user-creation form same-origin. That creates a second administrator account with an attacker-chosen password and REST API key. It is fixed in Dolibarr 24.0.0.
Impact of CVE-2026-71503
The one prerequisite is a logged-in administrator who opens the link once, the UI:R in the vector. That bar is low. The URL can arrive by email, ticket, or chat, and it looks like an internal setup page.
What the click yields is durable. The second administrator survives the browser closing, because it is a real database row rather than a hijacked session. The password and the API key are both attacker-chosen, so an operator who resets passwords in response but never audits llx_user.api_key leaves the REST path open.
The user directory is readable at once. Module-scoped records such as invoices, third parties, and salaries stay gated by User::hasRight() (htdocs/user/class/user.class.php:911), which has no administrator bypass, so those do not fall in the same breath.
The account is still a full administrator. One further request self-grants any permission, because $user->admin alone satisfies $caneditperms at htdocs/user/perms.php:69. Dolibarr is the accounting, invoicing, HR, and payroll system for a whole small business, now holding a second owner nobody provisioned.
And it does not depend on Products. 80 files include the same core template, and the reflection was confirmed on the Agenda and Third-parties setup pages, so disabling one module only moves the door.
How CVE-2026-71503 Enables Dolibarr Admin Account Takeover
A logged-in administrator opens a link that looks like an ordinary Dolibarr setup URL. Nothing on screen changes, but a second administrator account now exists in llx_user with admin=1, a password the attacker chose, and a REST API key the attacker chose. That key returns HTTP 200 from GET /api/index.php/users seconds later.
The flaw is a reflected cross-site scripting bug, patched in Dolibarr 24.0.0. This is the one finding in our Dolibarr research that is not an access-control gap.
The rest of this research documents a recurring authorization pattern we named split-brain authorization, two surfaces writing the same rows and disagreeing about who may. This bug is the odd one out. It is an output-encoding mistake rather than a missing permission check, and it still earns its place by the same shape underneath. The safe primitive already exists in the codebase and this path never calls it.
The Dolibarr Reflected XSS Root Cause
The injected value is the type query parameter, landing inside a single-quoted JavaScript string literal at htdocs/core/tpl/admin_extrafields_add.tpl.php:153.
GETPOST(..., 'alpha') is an input filter. It is not an output encoder, and that distinction is the whole bug. The alpha branch (htdocs/core/lib/functions.lib.php:1443) strips HTML tags, ../, and the double quote and its entity spellings. It never touches the single quote, which is precisely what closes this literal.
The right tool was one file away. dol_escape_js() (htdocs/core/lib/functions.lib.php:2286) exists to make a string safe for a JavaScript context, and its mode 1, documented in the docblock at line 2282, escapes the single quote for a '...' literal.
Dolibarr already calls it on a single-quoted string at htdocs/public/payment/newpayment.php:2891. The template echoed the raw filtered value instead. The missing guard is one call at one echo. Nobody had to write a new function.
How the CVE-2026-71503 Attack Chain Works
Reaching the sink needs only action=create and a type value. On the Products extra-fields page the element type is hardcoded (htdocs/product/admin/product_extrafields.php:53), the page guard at line 55 admits administrators only, and the template is included at line 105 when $action == 'create'. So an administrator's own GET renders the payload.
Two default behaviors keep the attack alive. First, create sits inside the $legitimate_actions allowlist (htdocs/main.inc.php:373-395), so the sensitive-GET CSRF check at line 396 never fires. This holds even with MAIN_SECURITY_CSRF_WITH_TOKEN at its strictest setting of 3, so a GET reaches a rendering path with no token required.
Second, MAIN_SECURITY_FORCECSP is empty on a fresh install and the header emits only when non-empty (htdocs/main.inc.php:1635), so no CSP constrains the injected script.
And that is where the token comes in. Dolibarr's anti-CSRF token is not regenerated per page load (htdocs/main.inc.php:344-360) and is published in every page's <head> as <meta name="anti-csrf-newtoken">, so the script reads it from its own DOM and holds a valid token for the form it wants to submit. The httponly cookie blocks nothing, because the browser attaches the session on a same-origin fetch.
Anonymous and non-administrator requests to the same URL reflect nothing. The sink is reachable only inside a privileged session, which is why it ships as a link to open.
CVE-2026-71503 Proof of Concept
Every request below ran against an isolated Dolibarr container we stood up at http://127.0.0.1:18080. No public or third-party install was ever contacted.
A bare probe first. Requesting product_extrafields.php?action=create&type=');alert(document.domain);// inside the administrator session returns HTTP 200, the response carries no Content-Security-Policy, and the inline <script> block now reads:
The literal is closed and the attacker's statements are appended as code. The weaponized type value then replaces the alert with a fetch to /user/card.php carrying action=add, the token read live from the meta tag, admin=1, an attacker-chosen password, and an attacker-chosen api_key.
Driving the logged-in administrator in headless Chromium, the POST returned 200 and a new llx_user row dol16pwn appeared with admin=1, the attacker's key encrypted at rest. The credentials were then used cold, three keys side by side:
The key that existed only because the victim clicked read the user directory, where two ordinary non-administrator keys were refused. The chosen password logged straight into /admin/modules.php. The created account was deleted through the UI afterward, and the key then returned 401.
CVE-2026-71503 Fix and Mitigation
The 24.0.0 patch closes the sink where it lives. The raw echo becomes dol_escape_js(GETPOST('type', 'alpha'), 1), and because it sits in the shared template, it repairs all 80 including pages at once.
A stronger version validates type against $type2label, the known extra-field types already loaded at product_extrafields.php:49. An allowlist cannot regress the way an escaper can be forgotten on the next new caller.
An input filter and an output encoder are not interchangeable. The alpha filter sanitized for HTML and left the character that mattered for JavaScript. Guard at the point of output, in the context of the output.
Ship a restrictive Content-Security-Policy by default rather than leaving MAIN_SECURITY_FORCECSP empty, so a single missed encoder is not the only thing standing between a reflection and code execution.
And reconsider what belongs in an allowlist like $legitimate_actions. The create action earning a CSRF exemption is what let a GET reach a privileged rendering path at all. Operators who cannot upgrade yet can set a CSP through MAIN_SECURITY_FORCECSP whose script-src drops 'unsafe-inline' and uses the nonce Dolibarr already emits.
Test the Attack Path
CVE-2026-71503 is a useful example of why vulnerability severity alone is not enough. The original flaw is a single reflected XSS, but the real impact appears only when the browser context, CSRF implementation, privileged workflow, and native account-creation endpoint are connected.
That is the gap between finding a vulnerability and proving an attack path. CodeAnt AI combines code-aware analysis with offensive testing to validate whether vulnerabilities are actually reachable and exploitable in a running application.
If you want to test your own application the same way an attacker would, start with a free CodeAnt pentest.
Related research: This finding joins our split-brain authorization pillar as its deliberate exception. It shares the same root shape, the safe path exists and this call site skips it, expressed as an encoding bug rather than an access-control one.
For the strongest of the access-control findings, see how an unauthenticated caller reaches customer records through the Third Parties REST API in CVE-2026-71505, where the web UI checks ownership and the API forgets to.
FAQs
What is CVE-2026-71503?
Is CVE-2026-71503 remotely exploitable?
Which Dolibarr versions are affected by CVE-2026-71503?
Can CVE-2026-71503 lead to account takeover?
Does the HttpOnly cookie protect Dolibarr against CVE-2026-71503?
How do I fix CVE-2026-71503?

Get Pentest Report
