CVE-2026-71506: Dolibarr Payment Deletion via Incorrect Authorization
CVE-2026-71506
CVSS 6.5

What if a clerk you only trusted to delete draft invoices could quietly wipe out a customer's recorded payments, and leave your books showing they still owe money they already sent?
That is what CodeAnt AI Security Research found in Dolibarr, the open-source ERP and CRM that runs invoicing and accounting for small and mid-size businesses.
CVE-2026-71506 is an incorrect-authorization bug in Dolibarr's payments REST API. The vulnerable Paiements::delete() route checks facture.supprimer (the permission to delete invoices) instead of facture.paiement, the permission that governs payments. The check runs. It just asks the wrong question.
It carries a CVSS score of 6.5. A non-admin holding only invoice-deletion rights can hard-delete recorded customer payments and inflate the apparent debt on those invoices, without ever holding the permission that actually controls receipts.
The fix is live in Dolibarr 24.0.0. If you run an affected build, stop reading and upgrade.
Now here is how we found it, and how it works.
The Check Ran, It Just Asked the Wrong Question
Dolibarr splits two permissions apart on purpose. One deletes invoices, the kind of access you hand a junior clerk cleaning up mistaken paperwork. The other manages payments, and it belongs to whoever reconciles the bank. The route that deletes a recorded payment was supposed to demand the second one. It demands the first. The check is there. It runs. It just names the wrong permission, and that one word is the whole finding.
So the clerk permission, the one deliberately kept away from the ledger, deletes real customer receipts. Every other payment path in Dolibarr gets this right. The web UI asks for the payment permission. The canonical API asks for it, and goes further, checking that the caller owns the invoices a payment touches. This one route asks for the wrong key.
Nothing about that is recoverable. The payment rows are hard-deleted, and the invoices they were settling drop back to showing the full amount outstanding, still marked validated, as if the customer never sent a thing. Severity lands at medium, CVSS 6.5. Dolibarr 24.0.0 closes it, see the table below.
Attribute | Details |
|---|---|
CVE | CVE-2026-71506 |
Affected component | Dolibarr payments REST API ( |
Vulnerability | Incorrect Authorization (CWE-863) |
CVSS | 6.5 Medium ( |
Wrongly required permission |
|
Correct permission |
|
Fixed version | Dolibarr 24.0.0 |
Discovered by |
Where This Started
At CodeAnt AI, we keep chasing one question: which open-source packages actually run real businesses, and what critical bugs are hiding inside them?
One of our researchers kept circling ERP platforms, and Dolibarr in particular, because an ERP is where a company's money actually lives. So we started reading the paths that move money.
Dolibarr is careful about this. It splits the authority to edit invoices from the authority to touch the payments recorded against them, and the money paths are supposed to demand the money permission.
We checked, and the careful places agreed: the web interface asks for it, and the canonical invoice API asks for it, even checking that the caller owns the invoices a payment is allocated to.
We didn't just look for missing checks. We compared the permission demanded by each payment operation against the permission that actually governs that operation elsewhere in Dolibarr.
So we asked the question that keeps turning up findings in this codebase: if every careful path demands the payment permission, does every path? We walked the payment routes one at a time. Then we reached the one that deletes a payment. It asked for the permission to delete invoices instead.
We handed it a key belonging to a low-privilege clerk, a key with invoice-deletion rights and nothing to do with money. The delete returned 200.
The payment row was gone, its allocation gone with it, and the invoice that had been part-paid now read zero paid against the full amount, still marked validated, as if the customer had never sent a thing.
That is where we landed.
How Dolibarr Splits Money From Invoices
Before the bug, the design it breaks.
Dolibarr deliberately keeps two permissions apart. One is the right to delete invoices, the kind of thing you'd hand a junior clerk so they can clean up mistaken or duplicate paperwork.
The other is the right to issue and manage payments, the actual money a customer has sent, which belongs to whoever reconciles the bank.
These are two separate, independently grantable permissions, and that separation is the point. It's how you let someone tidy invoices while keeping them away from the ledger.
Every careful path in Dolibarr honors it.
The web interface asks for the payment permission on both creating and deleting a payment.
The canonical REST endpoints agree, and one of them goes further still: it checks that the caller actually owns the specific invoices a payment touches, not just that they hold the right permission in general.
Two doors write the same payment records, the web UI and the API, and they're supposed to demand the same key. One of them had the wrong lock fitted.
Affected code
Payment creation and deletion in the web UI, both correctly gated on
facture.paiement:htdocs/compta/paiement/card.php:73(create),:132(delete)Canonical REST endpoints, all correctly gated on
facture.paiement:htdocs/compta/facture/class/api_invoices.class.php:1752, 1879, 2022The object-level check that confirms the caller owns the specific invoices touched:
api_invoices.class.php:2036-2042(putPayment(), viagetBillsArray()and_checkAccessToResource('facture', $id))
What We Found
Here's the method that has the wrong lock on it, trimmed to the load-bearing lines.
The primary flaw is the wrong permission check. Line 229 demands the right to delete invoices, where the rest of the codebase demands the right to issue payments. The check was right there. It just named the wrong permission.
The route also lacks the object-scope authorization used by the canonical payment path, the check that confirms you own the invoices a payment touches.
That's a real gap, but it's secondary to the headline issue: even with that check in place, a caller holding the wrong permission should never have reached this far at all.
There's a second bug we observed in the same method, separate from the authorization flaw. The underlying delete function never actually returns 0, so the first branch above is dead code.
On a successful delete, control falls through into the second branch and causes the method to attempt the deletion a second time. That double call left a fingerprint in our testing: a bank-linked control payment was refused with a 500 traced to the second branch, not the first, confirming the method really does try to delete twice.
Affected code
Vulnerable route, primary flaw:
htdocs/compta/facture/class/api_paiements.class.php:227-250,Paiements::delete(), checksfacture.supprimer(line 229) instead offacture.paiementSecondary gap: missing the object-scope loop that
putPayment()runs elsewhere in the same fileSeparate implementation bug in the same method: the underlying delete function never returns 0, making the first branch dead code:
htdocs/compta/paiement/class/paiement.class.php:699
What Actually Disappears
Deleting a payment in Dolibarr does more damage than it looks like, because of how the app stores what a customer has paid.
An invoice does not carry its own paid figure. What a customer has paid is the running sum of the allocation rows in a separate table. So deleting a payment does not cleanly mark an invoice unpaid. It silently raises the balance the customer still appears to owe.
In our test, three invoices each carried 1000.00 with one recorded payment of 110.00, 220.00, and 330.00. After three deletes, the payment rows and their allocation rows were gone, and all three invoices read 0.00 paid against 1000.00 outstanding while still marked validated, never settled.
Nothing was reversed. The financial records were hard-deleted, with no tombstone left behind on this path.
Proof of Concept
Every request here was fired at an isolated Dolibarr container on loopback: no public host, no third party, nothing but a Docker instance built to be thrown away afterward.
The actor is an internal, non-admin user (admin = 0) whose rights we read straight out of the database rather than take on trust: exactly the invoice-read and invoice-delete permissions, nothing else. No payment permission.
Control. Prove the payment permission is actually required elsewhere. A principal holding only third-party read rights sends the identical DELETE and is refused 403, confirming the guard is real and firing.
Then the actor offers its own key to the canonical payment-update route. Dolibarr refuses that too, forbidding this same user from so much as renaming a payment.
Exploit. Send DELETE using only the invoice-deletion permission. The request is unremarkable:
The response:
We ran it for the other two payment ids. Three 200s, three payments gone.
Confirm. Verify the payment and allocation rows are gone. A database read-back showed zero remaining rows in either the payments table or its invoice-allocation table, and all three invoices back at 0.00 paid.
One detail stings: one of the deleted payments had already been handed to the accounting export. A payment already exported for bookkeeping was destroyed by a caller who couldn't have even renamed it seconds earlier.
Why This Matters Beyond This One Route
Separation of duties is exactly why the invoice-deletion and payment permissions are kept distinct. You let a junior clerk clean up mistaken or duplicate invoices, and you deliberately keep them away from recorded receipts, which belong to whoever reconciles the bank.
Read the permission by its plain name, and invoice deletion says nothing about money. This flaw quietly folds payment destruction into it.
That's a practical abuse primitive: one request per payment can remove receipts, against ids the account may enumerate legitimately, leaving open invoices dunning customers for money they already paid, and ledger entries pointing at receipts the ERP no longer knows about.
Unless the optional Unalterable Archives module is on, and it's off by default, nothing reaches the event log at all.
This is one node in a pattern we saw across Dolibarr's REST surface: two write paths into the same set of rows that end up disagreeing about who may touch them.
We call it split-brain authorization, and it turns up across nine findings from this research effort. Its closest sibling here is CVE-2026-71510, where Dolibarr's sqlfilters parser leaks column data one boolean at a time. Both are a check that genuinely executes, and then decides the wrong thing, seen from opposite sides.
What This Finding Actually Shows
CVE-2026-71506 wasn't caused by a missing authorization check. Dolibarr checked a permission before deleting payments, and the check genuinely runs.
The failure was more specific: it checked facture.supprimer instead of facture.paiement. That's the difference between an authorization check existing and an authorization check being correct.
We found the mismatch by comparing the permissions demanded by related payment operations, then validated the impact against a running instance.
The result was that a low-privilege invoice clerk could delete recorded customer payments without ever holding the payment permission.
Are You Affected?
If you run Dolibarr through the affected builds and any non-admin holds the invoice-deletion right, that user can delete payments through the REST API today. Upgrade to Dolibarr 24.0.0.
The fix is small, because the correct behavior already existed twenty lines away: align the permission check with the payment right, add the object-scope loop after the fetch, and capture the delete result once so the routine stops running twice. The 24.0.0 release closed the gap.
The broader lesson is the one this whole cluster keeps arriving at. When two surfaces independently write the same records, the permission each one demands drifts apart unless something forces them to agree. A single table-driven map from object action to required right, consulted by both layers, is what stops a route from quietly asking for the wrong key.
If you want to test your own application the way an attacker would, start with a free CodeAnt pentest.
Related research. This finding is one node in the split-brain authorization pattern across Dolibarr's REST surface. Its closest sibling is CVE-2026-71510, where the sqlfilters parser leaks column data one boolean at a time, the same failure mode from the opposite side.
[FAQ]
Frequently Asked
Questions
What is CVE-2026-71506 and why is it classified as CWE-863?
What is the impact of CVE-2026-71506?
What is incorrect authorization (CWE-863), and how does CVE-2026-71506 illustrate it?
What privileges does an attacker need to exploit CVE-2026-71506?
Which Dolibarr versions are affected, and how is CVE-2026-71506 fixed?
What can an attacker do with CVE-2026-71506?
How is a wrong-permission check different from a missing authorization check?
More from CodeAnt
[GET STARTED]
START PENTEST






