DEV Community

Amartya Jha for CodeAnt AI

Posted on Originally published at codeant.ai

CVE-2026-71507 · Change one number, and the payroll run pays you

CVSS 6.5 · Object-level auth bypass → SEPA payment redirection (CWE-639) · Fixed in Dolibarr 24.0.0

You can't see a supplier's account, but you can quietly edit the bank number the company will pay. When payday's batch is generated, your account is sitting inside it as the payee.

The attack in six steps

  1. You can't even read the three suppliers. They're off-limits to your low-privilege account.
  2. But the bank-detail write route skips that ownership check. The same gap, on the field that matters most. (this is the break)
  3. Rewrite their bank numbers to accounts you control. Three IBANs, swapped for three of your own.
  4. The business generates its payment batch. The routine SEPA file it hands the bank.
  5. Your accounts are the creditors in a real payment file. Several thousand euros, in a byte-for-byte legitimate run. → money redirected.
  6. 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 is money leaving the building, and almost nobody reads it line by line. They trust that the numbers in it came from supplier records set up long ago.

This is the story of how an attacker who could not even read those supplier records rewrote the bank details inside them, so that when the next payment run was generated, three of the creditors were accounts the attacker controlled.

The same missing authorization check we found across Dolibarr shows up here on the one field where it hurts most: the destination of a payment.

This finding is a clear example of broken object-level authorization (BOLA) in an API: the application checks whether an account can perform a type of action, but fails to verify whether it can perform that action on the specific supplier 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 closes it

The fix in 24.0.0 brings the bank-detail write under the same ownership check the rest of the supplier record already enforced, so an account that cannot read a supplier can no longer quietly edit where that supplier gets paid.

The lesson we wrote down 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. Ask, for every value a low-privilege caller can change, where does this end up? This one ended up in the file that moves the money.

That is also why effective application security testing needs to trace an attack beyond the first successful request. The question is not only whether an API lets an unauthorized change through. It is what that changed value can eventually control.


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)