DEV Community

Amartya Jha for CodeAnt AI

Posted on Originally published at codeant.ai

CVE-2026-71506 · The wrong key on the lock

CVSS 6.5 · Wrong-permission authorization (CWE-863) · Fixed in Dolibarr 24.0.0

The button labelled “delete a draft invoice” is wired to the vault that holds recorded payments. Press it and money that was actually paid shows up as owed again.


The attack in six steps

  1. Deleting a payment should need the "manage payments" right. A specific permission for a specific role.
  2. But the route checks the "delete invoices" right instead. A different key for a different job. (this is the break)
  3. The attacker holds only "delete invoices". We dumped its real rights first to prove it.
  4. Delete a real, recorded customer payment with no check on which invoice it belongs to.
  5. The payment row simply vanishes. An invoice's "paid" total is just the sum of its payments.
  6. A fully-paid invoice flips back to "owed". Phantom debt on the books. → corrupted accounts.

In an accounting system, a recorded payment is not a note you can casually erase. It is part of the ledger, the line that says this invoice was settled, this money arrived. Deleting one should be a careful, privileged act, gated behind the specific right to manage payments and handed only to the people who reconcile the books.

This is the story of a route that guarded that destructive act with the wrong key, and how an account that could only ever “delete invoices” ended up destroying recorded payments and writing phantom debt onto the books.

The two permissions sound close enough that the mix-up is easy to make. “Delete invoices” and “manage payments” are different rights, handed to different roles for different reasons. The delete-payment route checked the first when it should have checked the second, a swapped lock on a door that leads somewhere it shouldn't.

Proving the account wasn't secretly privileged

Deleting money is destructive, so before anything else we did the unglamorous, essential step: we dumped the test account's actual rights and wrote them down. That matters, because the easiest way for a finding like this to be waved away is “well, your account probably had extra permissions.” It did not. It held the right to read and delete invoices, and nothing to do with payments at all.

Then we pointed that account at a real, recorded customer payment and asked Dolibarr to delete it. Two questions should have stopped the request. The first, “do you hold the right to manage payments?”, was never asked; the route checked the invoice-deletion right instead. The second, “are you even allowed to touch the specific invoice this payment belongs to?”, was not asked either. The payment simply vanished.

That distinction is at the heart of effective authorization testing. It is not enough to confirm that an API checks a permission. Security testing has to establish that the application checks the permission that actually governs the requested action.

Where the phantom debt comes from

Here is the quietly nasty part. In Dolibarr, an invoice's “paid” figure is not stored as a fact of its own. It is the sum of its payments. Remove one, and the sum drops, automatically, silently, with no separate record that anything was deleted.

So a fully-paid invoice, the moment its payment is destroyed, flips back to showing an outstanding balance. On the screen, and in every report that reads from it, money that genuinely arrived now looks owed. Delete the payments across a batch of invoices and you have manufactured debt that never existed, on books that will be trusted by whoever reads them next.

Why it lands harder than it looks

On paper this is a medium-severity bug, and in isolation that is fair. It does not hand anyone the keys to the system. But sit with what it does to a set of real books. Payments disappear with no audit trail. Paid invoices reopen. Reconciliation, the monthly ritual of matching what the bank says against what the system says, stops adding up, and the people doing it have no deleted-payment record to point at, because there isn't one.

The proof we kept coming back to was almost mundane, and all the more convincing for it: watching an outstanding balance climb on an invoice that had been paid in full, driven entirely by an account that was never supposed to be anywhere near the payment ledger.

This is exactly the kind of application security issue that can look harmless when reviewed only at the endpoint level. The permission check appears to be there, but it governs the wrong operation. A penetration test that exercises the action with deliberately mismatched privileges can expose that gap before it reaches production.

What closes it

The fix in 24.0.0 is exactly what the description implies: the delete-payment route now checks the right that actually governs payments, not the one that happens to sit nearby, and it verifies access to the specific record before acting.

The general lesson is one every codebase can use. When a destructive action is guarded, check that the lock on it is the right lock, that the permission being tested is the permission that governs the thing being done. A guard that checks a plausible-but-wrong right feels like security and is worse than none, because it looks solved.


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

← Back to all nine findings

Top comments (0)