CVSS 6.5 · Sensitive data exposure (CWE-200) · Fixed in Dolibarr 24.0.0
Every API reply is supposed to pass through a redactor that blacks out secrets before it goes anywhere. The Users redactor blacks out the password correctly. The Members redactor, same job, one desk over, forgot that single line, and hands the password verifier out along with the card.
The attack in six steps
Every API reply runs through a "cleaner": Its job is to strip secrets before anything is sent back.
Line the two cleaners up side by side: The one for users against the one for members. (this is the break)
The Users cleaner removes the stored password hash: Exactly as it should.
The Members cleaner doesn't: One missing line, doing the identical job one desk over.
A "read members" key receives every member's password verifier: And from the list route, all of them at once.
Confirmed live, not a placeholder: Matched to the database, and it validated the real password. → credential exposure.
Before an application hands any record back to a caller, something has to decide what stays out of the reply. Passwords, keys, the private machinery of an account, all of it is supposed to be stripped on the way out by a step whose only job is redaction. When that step is thorough, nobody ever thinks about it. When it isn't, secrets walk out in plain sight.
This is the story of two of those redactors doing the same job for two similar kinds of record, where one remembered to black out the password and the other simply didn't, and how that single missing line handed every member's password verifier to anyone allowed to read a member card.
It's a plain example of sensitive data exposure inside an otherwise legitimate API response: the endpoint works exactly as designed, and the security check meant to strip credential data is simply incomplete. It's also the same family of credential exposure as the mass-assignment bug that let a membership account reset Dolibarr's admin password elsewhere in this series: two features guarding the same kind of secret, enforcing two different rules.
Found by holding two things side by side
This finding didn't come from a clever attack. It came from comparison. Dolibarr redacts a user record and a member record with two separate pieces of code meant to do the same thing. So we put them next to each other and read them line for line.
The user redactor removed the stored password verifier before sending, exactly right. The member redactor, doing the identical job one desk over, didn't. One missing line, in one of two near-twin functions.
This is the kind of issue that can be missed by a straightforward application security assessment. Nothing needs to be injected or bypassed in the traditional sense. The vulnerability appears when two pieces of code responsible for protecting the same sensitive data are compared and found to enforce different rules.
What that one line leaks
The consequence is not subtle. An account holding nothing more than the right to read members, an ordinary, widely-granted permission, receives, along with each member's ordinary details, the stored verifier for that member's password. And because the same gap sits on the route that lists all members, you don't get them one at a time. You get the whole set in a single reply.
We confirmed the value was the real thing rather than a harmless placeholder by matching it back to the database and checking it corresponded to the member's actual password. It was live.
For a security researcher or penetration tester, that distinction matters. This was not simply a field that looked sensitive in a source-code review. We verified that the API returned real credential material and traced it back to the underlying application data.
The whole pattern in one line
If you want the entire theme of this research compressed into a single example, it is this one. The safe way already existed. The correct redactor was written, working, and sitting a file away. The neighbouring code that did the same job just skipped it. Not a hard problem, not a clever oversight, just a copy that wasn't quite a copy.
That is what "split-brain authorization" looks like at its smallest: two places responsible for the same rule, quietly disagreeing, with no one comparing them until someone does.
What actually closes this
The fix in 24.0.0 brings the member redactor in line with the user one, so stored password material is stripped before any member record goes out, from single-record and list replies alike.
The lesson here is almost embarrassingly practical: when two parts of a codebase are supposed to enforce the same rule, the highest-value test is often not an exploit at all, it's reading them side by side and asking whether they actually agree. One redactor got it right. Diffing it against its neighbor is how you find the one that didn't, and that kind of comparison is exactly what secure code review tooling is built to automate at scale, catching the moment a near-identical function quietly drifts from the rule its twin already enforces, long before it ships.
It's a fitting note to close this series on. Across all nine findings, from an admin account minted through a single link to this last credential leak, the pattern was never a single dramatic exploit. It was the same small gap, in different shapes, between what one part of the application enforces and what a nearly identical part quietly skips. Static application security testing and API penetration testing catch different halves of that gap. Together, they're what actually closes it.
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.


