CVSS 8.1 · Broken object-level authorization (CWE-639) · Fixed in Dolibarr 24.0.0
One clerk checks your ID before handing over a file. The clerk next to them, the one who resets that file's password, never asks, so you can change the lock on a door you're not even allowed to open.
The attack in six steps
Try to read a company you don't own: Your low-privilege key is correctly refused. (403 Forbidden)
Call the write route instead: The path that sets that company's customer-portal password. (this is the break)
It only asks: can you create companies?: Never "is this company yours?"
Set the portal password to one you choose: The write lands on a company you can't even see. (200 OK)
Sign into the customer portal as them: With the password you just set.
Read their invoices: Records you were forbidden to read directly. → another company's private billing.
Dolibarr gives each of a company's customers a small self-service portal, somewhere to log in and see their own invoices and nothing else. That only works if Dolibarr keeps every customer's portal account genuinely walled off from every other customer's, since the whole point of a shared back office is that no one gets to see a rival's or a stranger's billing.
This finding is about an account that couldn't so much as glance at another company's record, and still walked away with that company's invoices, by using a completely different, unguarded door to reset the portal password on an account it was never allowed to look at.
The account itself is intentionally weak. It can create new companies in Dolibarr; it cannot read the ones it doesn't own. Ask it to open a rival's record directly and Dolibarr correctly shuts the door. The read side of this feature holds up exactly as it should. The write side is a different story, and it's the same failure mode behind a lot of IDOR vulnerabilities: the check that should gate access to one specific record simply isn't sitting where you'd expect it.


The door that checks, and the door that doesn't
Before this finding we had already gone over the customer-portal read paths, the ones that hand information back, and found them careful. Ask to read a company you do not own and you are turned away, per company, correctly. It would have been easy to tick the box and call the whole area safe.
The discipline that paid off was refusing to assume the write paths behaved the same way. Reading and writing are different doors, even when they lead to the same room, and they have to be tested separately. That distinction is central to good API security testing: an authorization control on a read endpoint does not prove that the corresponding write endpoint enforces the same boundary.
The write path in question sets a company's portal password. And it asks a much lazier question at the entrance. Instead of "are you allowed to touch this company?", it asks only "are you the kind of account that can create companies at all?" The attacker's account can create companies, so it sails through. It never has to prove the company is its own.
Setting the lock on a door you can't see
So the attacker takes a company it was just told it may not read, and writes a brand-new portal password onto it. Dolibarr accepts the write. Then the attacker walks around to the customer portal, signs in as that company with the password it just set, and the invoices, the ones meant to be private to that customer forever, open up on screen.
The read wall was never breached. It was simply walked around, through a write path that forgot to ask the same question.
That is the defining shape of a broken object-level authorization (BOLA) vulnerability. The API knows that access should be restricted at the object level, but one route checks only a broad permission instead of whether the caller is authorized to access the specific company identified in the request.

Why this shape keeps happening
This is one of the cleanest examples of the pattern that runs through most of our Dolibarr security research. Enforcement on the way in tells you nothing about the way out. A team hardens the read paths, watches them hold, and reasonably assumes the feature is safe, while a sibling write path, added at a different time by a different hand, checks a weaker condition, or none at all.
The impact is concrete and it compounds. Every company with a portal account is exposed, not just one. An attacker who can create companies, a low bar in many deployments, can reset portal passwords across the whole customer base and read each customer's private billing in turn. For a business whose customers trust it to keep their invoices to themselves, that is exactly the kind of quiet access control vulnerability that does lasting damage.
This is also why BOLA belongs in API penetration testing rather than being treated as a purely theoretical code issue. The important question is not only whether an endpoint contains an authorization check, but whether the check actually protects the specific object the request is trying to modify.
What actually closes this
The fix in 24.0.0 makes the write path ask the exact ownership question its read sibling already asks: is this caller actually allowed to touch this specific company? Same check, now enforced on both doors instead of just one.
The lesson we keep relearning across this series is the same one: test read and write as separate surfaces, every time. A boundary that holds where you happened to look first isn't evidence it holds everywhere it needs to. It's evidence of one door, out of however many lead into the same room, which is exactly the case for treating API penetration testing as an ongoing discipline rather than a single pass over the obvious endpoints.
This also isn't the only place in the Dolibarr assessment where a write path trusted a broad permission instead of checking the specific object in front of it. We found the same shape in how supplier payments could be redirected, just with a different object sitting on the other end of the request.
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.


