CVSS 9.3 · Reflected XSS (CWE-79), chained to admin creation · Fixed in Dolibarr 24.0.0
Think of a contractor's badge that, when scanned at reception, silently prints a second master key, and the guard hands it over, because the request came from inside the building.
The attack in six steps
- A link that looks like ordinary work: A URL pointing at a routine admin setup page, with a tiny instruction hidden in the web address.
- An admin opens it: It arrives by email, ticket or chat and looks like an internal settings page; nothing on screen changes.
- The page runs the hidden instruction: It drops that address value straight into its own scripting, missing one safety step, so the attacker's snippet runs as code. (this is the break)
- The script reads the page's own security token: The anti-forgery token Dolibarr prints into every page for itself; the injected script just reads it, no theft required.
- It asks Dolibarr to create an admin: Using that token and the admin's own logged-in session, a create-user request that looks completely legitimate.
- A second administrator now exists: With a password and API key the attacker chose, surviving after the browser closes. → full admin.
Dolibarr is the quiet software at the centre of a lot of small companies. It holds the invoices, the customer list, the supplier bank details, the payroll, the whole back office. The person who administers it can do anything: add users, change settings, read every record. So the single most valuable thing in the entire system is the administrator's account.
This is the story of how an attacker walks away with a second administrator account, one Dolibarr never knew it was creating, from a single link, opened once, by an admin who was only doing their job.
The link looks utterly ordinary. It points at an internal setup page, the kind of screen an admin opens a dozen times a week to add a custom field. Nothing on that page looks wrong. Nothing flashes. The admin glances at it and moves on. And in that glance, a new administrator quietly appears in the database, with a password and an API key the attacker chose in advance.
Here is the part we love about this one: the obvious version of the attack failed, and that failure is exactly what made the dangerous version possible.
The reflex that didn't work
When you find a bug that lets you run a snippet of code on someone else's page, the first instinct is boring and predictable: steal the login cookie, become that user. We tried. It came back empty.
Dolibarr, sensibly, hides its session cookie from scripts, a setting that tells the browser "code running on this page may not read this." So the textbook move was dead on arrival. On most days, that is where the story ends and the bug gets filed as low severity: yes, you can run a script, but there is nothing here worth taking.
From an application security testing perspective, this is an important distinction. Finding reflected XSS does not automatically tell you what the vulnerability can achieve. The real question is what security-sensitive actions the injected code can perform within the victim's authenticated session.
The moment it turns
Except there was something worth using, not stealing.
To protect its own forms, Dolibarr prints a small security token into every page. It is an anti-forgery measure: the app's own buttons read that token and send it back, so the server can tell a genuine click from a forged one. It is a good defence. But a token printed into the page for the page's own scripts to read is, by definition, readable by any script on the page, including the one the attacker just slipped in.
So the injected script does not need to steal anything. It reads the token sitting right there in the page, and then it does the single thing that token unlocks: it asks Dolibarr to create a new user, as the admin, from the admin's own logged-in browser. To the server it looks like the administrator calmly filling in the "add user" form. The browser attaches the admin's session automatically, because the request is going to the very same site it came from. The request came from inside the building, so the guard waves it through.
And no alarm sounds, for a mundane reason: a fresh Dolibarr install does not switch on the browser-level protection that would have refused to run the injected script in the first place. It is off by default, so nothing stands in the way.
What the attacker walks away with
The important thing about that new account is what it isn't. It is not a hijacked browser tab that vanishes when the admin logs out. It is a real administrator, a real row in the database, with a password the attacker chose and an API key the attacker chose. It stays there after the browser closes, after the laptop sleeps, after the admin goes home.
That one detail changes the entire clean-up. Picture the team noticing something odd and doing the sensible thing: reset the admin's password. It does not help. The attacker was never using the admin's password, they made their own account. And the API key on that account keeps working quietly in the background, reading the whole user directory through Dolibarr's own interface, long after everyone assumes the incident is closed.
There is a limit worth being honest about: some records, specific invoices, customers, payroll, stay behind Dolibarr's normal permission checks, which have no special "admins skip this" bypass. But a full administrator can grant itself any permission it likes with one more request, because simply being marked an administrator is enough to unlock that. So the limit is a speed bump, not a wall.
And it is not about one page or one feature. The vulnerable screen is built from a template Dolibarr reuses across 80 different pages. We confirmed the same trick on the Products, Agenda and Third-parties setup screens. Turning off one module only moves the unlocked door somewhere else.
Why it was there, and what closes it
The frustrating part is that Dolibarr already owned the tool that would have stopped this cold. It has a function whose entire job is to make a value safe to drop into a script, and it is used correctly elsewhere in the same codebase. This one screen simply never called it. The value from the web address was filtered on the way in, but filtering an input for a few bad characters is not the same as safely escaping it on the way out. One missed step, on one line, behind 80 pages.
Dolibarr 24.0.0 fixes it: the value now passes through the proper escaper before it ever reaches the page, and because the fix lives in the shared template, all 80 pages are protected at once. If you run an affected build, upgrading is the whole answer.
This finding is the odd one out in our Dolibarr research. The rest of that work is about permission checks that quietly go missing between two different doors into the same room. This one is an encoding mistake, not an access-control one, yet it earns its place for the same underlying reason: the safe thing already existed, one function call away, and a single screen skipped it.
For vulnerability research and penetration testing, that distinction matters. A source-code review can identify the unsafe sink, but testing the running application shows whether the flaw can actually be chained into a meaningful security impact.
The fix. Upgrade to Dolibarr 24.0.0, that closes this finding. Want to know where your own app disagrees with itself? → Start with a free CodeAnt pentest



Top comments (0)