CodeAnt AI Security Research
CVE-2026-71511: Dolibarr Password Hash Disclosure

Amartya Jha
CEO, CodeAnt AI
TL;DR
CVE-2026-71511 is an insufficiently-protected-credentials flaw (CWE-522) in Dolibarr ERP CRM's Members REST API, scored CVSS 6.5. The response cleaner strips two password fields, pass and pass_indatabase, but not the third, pass_indatabase_crypted, which carries the member's live bcrypt hash. Any caller holding only the adherent.lire read-members right receives that hash from GET /api/index.php/members/{id} and, per record, from the bulk GET /api/index.php/members. It is fixed in Dolibarr 24.0.0.
What is CVE-2026-71511?
CVE-2026-71511 is an insufficiently-protected-credentials flaw (CWE-522) in Dolibarr ERP CRM's Members REST API. The serialization cleaner that runs before every member response strips two credential fields, pass and pass_indatabase, but not the third, pass_indatabase_crypted, which is the actual stored bcrypt hash from the llx_adherent.pass_crypted column.
A principal authenticated with an ordinary per-user API key, holding only the adherent.lire (Read members' card) right and no administrator flag, receives that verifier in the body of GET /api/index.php/members/{id} and, in bulk, from GET /api/index.php/members.
The severity is Medium, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N, scoring 6.5. It was found on the develop branch at 25.0.0-alpha (commit ab7e60406d) and fixed in the 24.0.0 release.
The confidentiality impact is real but it has a ceiling, and it is worth stating plainly. What leaks is a bcrypt verifier, not a plaintext password. Nobody's session is handed over.
The value has to be cracked offline before it becomes a login. Offline is exactly where bcrypt gives up its home-field advantages, which is the second half of why this matters.
Why Dolibarr's Members API Exposes the Password Hash
This is not a check that someone forgot to invent. It exists, fully formed, twenty-odd files away, with a comment on it.
The User REST API's cleaner, in htdocs/user/class/api_users.class.php:1316-1323, opens with a line that could not be more explicit about intent:
Whatever are permissions. The User API treats the whole password-hash family as unspeakable, stripping it for administrators too, and it doubles down on the write side, rejecting those same fields as assignable input at api_users.class.php:360 and :442. That is the policy, written by the project, in the project's own words.
This is the split-brain shape we traced across nine Dolibarr findings. Two surfaces reach the same kind of record and disagree about the rules, because the correct behavior lives in one sibling and the other path simply never runs it.
Here the disagreement is not even about who may read. It is about what a lawful read is allowed to contain. The Members cleaner passes the authorization check honestly and then forgets the redaction the User cleaner spells out line by line, and that is what makes CVE-2026-71511 an oversight rather than a deliberate feature.
How the Dolibarr Members API Leaks the Bcrypt Hash
The hash is loaded eagerly. Adherent::fetch() adds d.pass_crypted to its SELECT at htdocs/adherents/class/adherent.class.php:1602, and at line 1664 copies it onto a public property declared back at line 90:
So by the time any route calls the cleaner, the live verifier is already a public field on the object. The Members cleaner, MembersApi::_cleanObjectDatas(), delegates to the shared base and then only ever removes non-credential clutter:
And the base cleaner it hands off to stops one line too early:
Two of the three password fields die. The one holding the real hash survives.
Both the checks that let you in are correct. get() gates on hasRight('adherent','lire') at api_members.class.php:76 and returns the object at line 94, and index() gates the same way at :285 before returning the list at :334.
The four alternate lookups (getByThirdparty, getByThirdpartyAccounts, getByThirdpartyEmail, getByThirdpartyBarcode) all end at the same cleaner, so they inherit the same leak.
CVE-2026-71511 Exploit: Reproducing the Password Hash Leak
Everything below ran against a self-contained Dolibarr in local Docker, seeded with fixtures we created. No external or shared system was in the loop at any point. The instance ran a hardened production profile, install locked, production mode on, HTTPS forced, and CSRF at its strictest, and none of that hardening touched this.
We provisioned one non-administrator user, f009_member_reader, whose entire effective right set was a single row, adherent.lire. No group membership, admin = 0, account enabled.
We gave a fixture member a known password through the ordinary admin write path, so the database column llx_adherent.pass_crypted held a fresh bcrypt string we controlled. Notably, the row's plain pass column stayed NULL, which means the bcrypt hash was the only piece of credential material the member row carried.
Because the profile forced HTTPS, every request went out with an X-Forwarded-Proto: https header. Without it the API answered HTTP 302, which is a transport quirk and not a control on this leak.
The controls came first. An anonymous request to the member endpoint returned HTTP 401, so authentication is required. A second valid key that authenticated but lacked adherent.lire was refused at the call stage with a 403 naming api_members.class.php:77, confirming that one right is the only gate.
Then the reader key:
HTTP 200, served by Restler v3.1.0, with the verifier beside the login it belongs to. The card carries 114 top-level properties in full, and it is trimmed here to what matters.
To be sure the string was a real credential and not a decorative placeholder, we compared it byte-for-byte against the pass_crypted column, identical, and ran password_verify() in PHP, which returned TRUE for the member's real current password and FALSE for a wrong one. It is a functioning verifier, retrieved with a read-only key.
Asking the collection route, GET /api/index.php/members, returned the same field for the one record on the instance. On a populated deployment that is one hash per member, per page.
Who Can Exploit CVE-2026-71511?
adherent.lire is not a privileged right. It is what you grant a membership secretary, a front-desk clerk, a reporting dashboard, or a CRM sync job, anyone who needs to list members but should never touch a password.
Nothing in the right's name or description hints at credential access, so an administrator handing it out has no reason to think they are also handing out the member password store.
Once the hashes are off the server, every defense that protects the live login is gone. There is no rate limit, no lockout, no failed-attempt counter standing between an attacker and an offline cracking rig, and every member who reused that password somewhere else is now exposed there too, with the leaked login and email as ready-made identifiers.
Bcrypt raises the per-guess cost, so this is not an instant break. Weak, common, and reused passwords fall to an offline wordlist run regardless, and the attacker gets to try forever, quietly, with no live system to notice.
There is also a quieter escalation, which we confirmed by reading the source but did not execute. User::create_from_member() copies $member->pass_indatabase_crypted into a new back-office user's pass_crypted at provisioning time (htdocs/user/class/user.class.php:2110).
Where a staff login was created that way and neither password has changed since, the leaked member verifier is also that staff account's verifier, an account that may carry far broader rights than the member ever had. We are flagging that as a source-level linkage, not a demonstrated exploit.
Finally, the read leaves almost no trace. On the tested instance it produced no llx_events row and no log line, appearing at most as a routine authenticated member read, so an incident responder cannot scope it after the fact.
How Dolibarr Fixed CVE-2026-71511
Dolibarr 24.0.0 does what the finding argues for. It extends the redaction so the hash family is stripped before serialization on the Members path, bringing the Members API in line with the never-returned-by-API, whatever-are-permissions rule the User API already enforced.
Revoking adherent.lire from any key that does not strictly need it, and treating already-exposed member passwords as compromised, is the operational follow-through on an install that ran a vulnerable build.
The general lesson is the one this whole class keeps teaching. A redaction list that lives inside one cleaner is a rule only that cleaner obeys, and the next API class that serializes the same model starts from zero and re-earns every omission by hand.
The durable fix is to make the safe thing structural. Put the credential-stripping in the shared base cleaner so no subclass can forget it, mark credential fields on the model so they never reach a serializer at all, and add a CI test that walks every REST class and fails the build if any property matching /^pass/ survives its cleaner. Then the guard is not a comment one file happened to write. It is a property of the system.
A Read Permission Should Never Expose Credentials
CVE-2026-71511 is not an authentication bypass or an authorization failure. The caller legitimately has adherent.lire access; the vulnerability is in what the API returns after granting that access. Dolibarr’s Members API exposes pass_indatabase_crypted, the live bcrypt password verifier, even though the User API explicitly treats password fields as properties that should never be returned. That makes CVE-2026-71511 a classic CWE-522 insufficiently protected credentials flaw.
The broader lesson is simple: API authorization does not end when access is granted. Response serialization is part of the security boundary. Every endpoint that returns shared user or member objects must enforce a deny-by-default policy for credentials, tokens, secrets, and internal security fields. Redacting a sensitive property in one serializer does not protect it when another endpoint can serialize the same object.
CodeAnt AI Security Research found CVE-2026-71511 during a broader assessment of Dolibarr’s authorization model. The finding demonstrates why agentic penetration testing must validate what an authorized request actually returns, not merely whether the request itself was permitted.
This research is part of an ongoing effort by CodeAnt AI Security Research to audit widely-used open-source packages for security vulnerabilities. We believe the open-source ecosystem deserves better tools, better auditing, and more support for the maintainers who keep it running. More findings will be published as patches ship and coordinated disclosure timelines are met.
If you are a maintainer and have been contacted by our team, thank you for your work. If you believe your package may be affected by a similar pattern, we’d love to help: securityresearch@codeant.ai
Related reading
This finding is one node in the split-brain authorization pattern, the pillar behind all nine findings, two doors into the same rows that disagree about the rules.
Its mirror image lives in the very same Members API. Where CVE-2026-71511 lets a low-privilege caller read a credential out, CVE-2026-71504 lets one write an admin credential in through mass assignment. Same file, opposite direction.
FAQs
What is CVE-2026-71511, and what does it leak?
Can the bcrypt hash leaked by CVE-2026-71511 be used to log in directly?
How does CVE-2026-71511 expose a back-office staff account?
What privileges does an attacker need to exploit CVE-2026-71511?
Which Dolibarr versions are affected, and how is CVE-2026-71511 fixed?
What data can be exposed through CVE-2026-71511?

Get Pentest Report
