CVSS 6.5 · Object-level auth bypass → SEPA payment redirection (CWE-639) · Fixed in Dolibarr 24.0.0
You're not allowed to open a supplier's record, yet you can quietly rewrite the bank number the company will pay into. By the time the next payment batch is generated, your own account is sitting inside it as the payee.
The attack in six steps
You can't even read the three suppliers: They're off-limits to your low-privilege account.
But the bank-detail write route skips that ownership check: The same gap, on the field that matters most. (this is the break)
Rewrite their bank numbers to accounts you control: Three IBANs, swapped for three of your own.
The business generates its payment batch: The routine SEPA file it hands the bank.
Your accounts are the creditors in a real payment file: Several thousand euros, in a byte-for-byte legitimate run. → money redirected.
We stopped at the generated file: Never sent it, but the attacker's account was already inside a live payment instruction.
Once a month or once a week, a business does something almost mechanical: it generates a payment file and hands it to its bank, and the bank pays every supplier on the list. That file is one of the most consequential things the whole system produces. It's money leaving the building, and almost nobody reads it line by line. Everyone trusts that the numbers in it came from supplier records set up long ago.
This is what happens when an attacker who couldn't even read those supplier records rewrote the bank details sitting inside them, so that the next time a payment run was generated, three of the creditors were accounts the attacker controlled.
It's the same missing authorization check we kept finding across this Dolibarr assessment, this time showing up on the one field where it hurts most: the destination of a payment. The same broken object-level authorization we found letting attackers reset a stranger's portal password shows up here on supplier bank details instead: the application checks whether an account can perform a type of action, not whether it can perform that action on this specific record.


The write that shouldn't have worked
The attacker's account is low-privilege by design. Ask it to read the three target suppliers and it is refused. It has no business seeing them. On the read side, Dolibarr holds the line.
But editing a company's bank details goes through a different API path, and that path skips the ownership check its read sibling performs. It asks whether you are the sort of account that can edit company records at all, not whether these companies are yours. So the attacker rewrites the account numbers of three suppliers it was never allowed to look at, swapping in three accounts of its own.
This is the kind of authorization gap that API penetration testing is designed to uncover. A basic permission check can look correct while still allowing a low-privilege user to modify another object's sensitive data. Testing the read and write paths separately exposed the difference.
Chasing the value to where it actually goes
A changed bank number, on its own, is the kind of thing a busy reviewer shrugs off. "So a field can be edited, so what." The move that turned this from a shrug into a serious security finding was refusing to stop at the write, and asking the only question that matters: where does this value actually go?
It goes into the payment file. When the business next generates its supplier-payment batch, the routine SEPA payment file it hands its bank, Dolibarr reads each supplier's default bank account and writes it into the file as the creditor. The three accounts now sitting in those supplier records are the attacker's. So the generated batch, a file that is byte-for-byte a legitimate payment run, carries the attacker's accounts as the people about to be paid, for several thousand euros.

Where we stopped, and why it still has teeth
We stopped at the generated file. We never submitted it to a bank, and no money ever moved. That line is not one a security test gets to cross. But the finding does not need money to have moved to be real. The attacker's account was already inside a live payment instruction, indistinguishable from a legitimate one, waiting for the ordinary approval such files get on their way out the door.
Whether that last step would succeed in the real world depends on a company's own controls, whether a human reconciles the file, whether the bank checks that the account name matches the account number. Those are real backstops. But they are backstops against a file that should never have contained an attacker's account in the first place. The boundary meant to prevent this, you may not touch suppliers you don't own, was already bypassed several steps upstream.
For Dolibarr security, the important lesson is that protecting a payment workflow cannot stop at the final payment-generation step. The supplier data feeding that workflow needs the same object-level authorization controls as the payment action itself.
What actually closes this
The fix in 24.0.0 brings the bank-detail write under the same ownership check the rest of the supplier record already enforces, so an account that cannot read a supplier can no longer quietly edit where that supplier gets paid.
The lesson worth keeping is about following data, not just guarding entry points. A tampered field is only as harmless as the least-careful place it eventually flows to. For every value a low-privilege caller can change, ask where it ends up. This one ended up inside the file that actually moves money, which is exactly the kind of business logic gap that a permission checklist alone will never catch, since every individual request along the way was valid and authenticated.
It's also a reminder that ERP security can't stop at the workflow that looks sensitive. Dolibarr's payment-generation step itself was never the weak point, the supplier data quietly feeding it was. Protecting the last step in a chain means nothing if an earlier, more mundane-looking field can be rewritten by someone who was never supposed to touch 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.


