AI Pentesting

API Penetration Testing: 9 Dolibarr Security Findings

Amartya | CodeAnt AI Code Review Platform
Sonali Sood

Founding GTM, CodeAnt AI

You can run security scanners across your codebase and still miss the vulnerabilities that matter most. A scanner can flag a missing authorization check or suspicious data flow. API penetration testing goes further by proving whether an attacker can actually exploit it and what that exploit can achieve.

We tested Dolibarr ERP CRM and found nine real vulnerabilities, including broken access control, mass assignment, IDOR, business-logic flaws, credential exposure, payment redirection, and reflected XSS. Each finding was traced from the vulnerable code to a working, observable impact: administrator takeover, cross-company invoice access, payroll manipulation, attacker-controlled payment accounts, and password-hash disclosure.

The research was conducted by CodeAnt AI Security Research using source-code analysis and testing against a running Dolibarr instance. The goal was not simply to assign a CVSS score, but to answer the question that matters during an API security assessment:

What can an attacker actually do with this vulnerability?

That distinction is the core of API penetration testing. The following nine findings show why exploitability, authorization context, and downstream business impact matter more than a vulnerability score alone.

9 Real Dolibarr Vulnerabilities Found in One Pentest

All nine live in Dolibarr, an open-source ERP and CRM written in PHP. It runs the accounting, invoicing, HR, and payroll of small and mid-sized businesses.

Eight are flaws in its REST API. One is a cross-site scripting bug in an admin template.

They were tested on version 25.0.0-alpha, commit ab7e604, on a hardened production profile. Install lock was on, production mode on, HTTPS forced, and CSRF protection at its strictest setting.

None of that configuration closed any of these. One finding, the payment deletion, was confirmed present in released 23.0.3 as well.

Read together, they tell one story. Dolibarr's web interface enforces a careful permission model, and its REST API, exposed at /api/index.php, quietly fails to reproduce it.

The result is a run of access-control failures that a scanner would rate anywhere from medium to critical, and that chain, in practice, to full takeover.

Finding

Class

CVE

CVSS

What it does

Reflected XSS to a rogue admin

Cross-site scripting

CVE-2026-71503

9.3 Critical

One admin-opened link creates a second admin with an attacker-chosen password and API key

Mass assignment to full takeover

Mass assignment

CVE-2026-71504

8.3 High

A membership clerk resets the administrator's password and owns the instance

IDOR to portal takeover

Broken object level authorization

CVE-2026-71505

8.1 High

Sets the portal password of a company it cannot read, signs in, reads its invoices

The wrong permission check

Incorrect authorization

CVE-2026-71506

6.5 Medium

Deletes recorded customer payments with only the "delete invoices" right

Authorization bypass to payment redirection

Authorization bypass

CVE-2026-71507

6.5 Medium

Rewrites supplier bank details that then land in the SEPA payment file

Mass assignment to payroll tampering

Mass assignment

CVE-2026-71508

6.5 Medium

Rewrites salaries and cost rates with no payroll permission at all

Self-approving expense claims

Business logic flaw

CVE-2026-71509

6.5 Medium

Approves your own claims into the payable queue, breaking separation of duties

A filter that reads what it cannot return

Incorrect authorization

CVE-2026-71510

6.5 Medium

Recovers salaries and a password-hash image through a search-filter oracle

Handing out password hashes

Insufficiently protected credentials

CVE-2026-71511

6.5 Medium

Returns members' live bcrypt hashes to anyone who can read a member card

Each of those links goes to a full technical write-up with the reproduction. The rest of this piece is about what they share, because the shared part is what a penetration test is actually for.

Why CVSS Scores Don't Prove Exploitability

Six of these nine carry a CVSS of 6.5. On a triage board that is a middle-of-the-pile number, the kind that sits under a stack of criticals and waits its turn.

The score is doing its job. It rates each bug in isolation, on the assumption that you will read them one at a time. An attacker does not read them one at a time.

Walk the membership-clerk finding, rated High on its own. It ends in a working administrator login from an account that started with membership rights and no user-management access whatsoever.

Walk the payment-redirection finding, a Medium. It ends with an attacker's IBAN inside a generated bank instruction for several thousand euros.

That file gets signed off by an approver who checks that the payees and amounts look right. They do look right. Only the account numbers changed.

The number describes the lock. It says nothing about whether the key is already in the door.

That distinction is the reason API penetration testing exists as a discipline separate from scanning. It is why CodeAnt's platform ranks findings by the path they open rather than the number they add to a backlog.

Here is the same idea, finding class by finding class. The left column is what a static tool can tell you. The right column is what testing had to prove.

Finding class

What a scanner reports

What the pentest proved

Reflected XSS

Input reaches a script sink

An admin-opened link mints a second administrator

Mass assignment

A request body is copied onto a record

A member-desk key logs in as the administrator

Broken object level authorization

A write route has no per-object check

A junior account reads another company's invoices

Authorization bypass

A bank field is writable

Attacker IBANs land in a real 6,600 euro SEPA file

Credential exposure

A response omits a cleaner call

Live password hashes are returned in bulk

The move that matters is in that right column every time. Carrying a finding to its impact is what separates the theoretical from the routine, and it is the work the rest of this piece is organized around.

How Low-Privilege API Keys Can Become Attack Paths

Before the mechanics, look at the attacker profile, because it is the same across almost all nine and it is low.

None of the API findings needs an administrator. None needs a stolen session, a CSRF token, or a pre-issued key beyond the attacker's own. Each one needs a single ordinary API key and a routine permission that real deployments hand out every day.

The membership takeover needs the right to create members. The portal and payment-redirection findings need the right to create companies, the kind a junior salesperson or a data-sync integration holds.

The payroll write needs nothing but the right to edit your own profile. The hash disclosure needs nothing but the right to read a member card.

Not one of the API findings needs a victim to do anything. The write completes inside the attacker's own request.

The reflected XSS is the single exception, and its cost to the attacker is one thing, a logged-in administrator opening a link, which is ordinary phishing. That is the whole bar for a chain that ends in a permanent admin account.

Here is the bar in one view, per finding. The access column is the whole prerequisite. The victim column is what the target has to do wrong.

Finding

Access the attacker needs

Victim action

Ends in

Reflected XSS

None. A crafted link

One admin opens it

A second administrator with attacker credentials

Members takeover

Create-members right, own API key

None

Admin password reset, full instance control

Portal takeover

Create-companies right, own API key

None

Another company's invoices read

Payment deletion

Delete-invoices right, own API key

None

Recorded payments erased, phantom debt

Payment redirection

Create-companies right, own API key

None

Attacker IBANs in the SEPA file

Payroll tampering

Edit-own-profile right, own API key

None

Salary and cost rates rewritten

Self-approval

File-expenses right, own API key

None

Claims approved into the payable queue

Filter oracle

List-users right, own API key

None

Salaries and a hash image read blind

Hash disclosure

Read-members right, own API key

None

Live password hashes returned in bulk

Low-privilege keys are exactly the keys that leak, that live in CI configuration, that get handed to a contractor for one integration and never revoked. The findings below are what those keys can reach.

The Authorization Failures Behind These API Vulnerabilities

Start with the eight API findings, because they rhyme. Dolibarr keeps most business objects, users, members, companies, invoices, on the same rows the web UI edits, and it exposes those rows over REST.

The web UI guards them with a mature permission model. The API, in most of these cases, checks one coarse permission and then trusts the request body.

That is the root cause, stated once. The three sections that follow are the three faces it wears. Each face is a distinct research move, and each one found more than one bug.

Mass Assignment: When the API Ignores UI Permissions

The cleanest version shows up in the Members module.

A Dolibarr member record can be linked to a real login account, and Dolibarr keeps some fields, including the password, synchronized between the two.

The members write route checks one thing: whether the caller may create members. It then copies every remaining key from the JSON body onto the member object.

Two of those keys are not member data the caller should control:

  • One specifies which user account the member is linked to.

  • One specifies a new password.




How the Privilege Escalation Happens

The route then updates the member with default synchronization flags, which means “sync the linked user account, password included.”

A non-empty password reaches the routine that:

  1. Loads the user named by the attacker-supplied ID.

  2. Rewrites that account's password.

  3. Performs no right check on the password change.

The permission that governs this, adherent.creer, is grantable with no user-management right at all.

So a clerk holding only membership rights can:

  1. Point the request at the administrator.

  2. Set a new password.

  3. Log in as the administrator using the password they just chose.

The expected behavior was a 403. Setting another user's password over the API should require the same specific right the web form demands.

Instead, the observed behavior was:

  • 200 response

  • Administrator's stored password hash replaced

  • Attacker's chosen password accepted

  • Admin setup page rendered to the attacker

This is CWE-915: improperly controlled modification of dynamically determined object attributes, the formal name for what most engineers call mass assignment.

The Consequence Is Immediate

The consequence is not subtle.

The real administrator is locked out at once, with the old password now failing with "Bad value for login."

Recovery needs an out-of-band database reset if no second administrator exists.

The visible trace reads like a forgotten password, which is exactly the sort of thing nobody escalates on a Tuesday afternoon.

The Same Pattern in Payroll

The same shape governs payroll.

Dolibarr stores salary, bonus, and hourly and daily cost rates as plain columns on the user record.

The web interface hides these five fields behind a payroll permission.

The user-update API, however, keeps out a short list of obvious secrets and lets everything else through. The five payroll fields are not on that list.

An employee whose only right is editing their own profile can therefore set their own:

  • Salary

  • Bonus

  • Hourly cost rate

  • Daily cost rate

  • Other payroll field in the five-field set

to any value in a single request.

That account has no legitimate path to payroll rights at all, which makes this a clean privilege-boundary crossing.

Why a Casual Test Can Miss It

To make the change quieter, the API response omits four of the five values it just wrote.

So a casual test can make it look as though nothing changed.

The database changed.

The corrupted figure then flows into the payroll export finance reads—a report gated on an export permission the attacker does not hold.

That is where the false assurance comes from: the attacker does not need access to the final report for the unauthorized write to matter.

Expense Reports: The Same Defect in a Different Workflow

Expense reports close the set.

The workflow is meant to separate the person who files a claim from the person who approves it, enforced through distinct permissions.

The dedicated approve route checks the approver right.

The generic update route, however, lets the caller write both the status field and the approver field directly.

So an employee who can only file claims can:

  • Mark their own claim approved.

  • Mark their subordinates' claims approved.

  • Push those claims into the queue of things to be paid.

The approver name recorded on the claim also comes directly from the request body, so the attacker can name someone else.

One tell survives: a blank approval date.

That happens because this path never stamps the field that the legitimate approval flow does.

Three Modules, One Research Move

Three modules. Three roles. One underlying defect.

Each is worth reading in full because the fix and the blast radius differ.

But the research move was identical every time:

Open the API class for an object the UI guards, and check whether the guard survived the trip.

Broken Object Level Authorization: The Read/Write Gap

The second face is subtler because the API does check per-object access. It just checks it in the wrong places.

Dolibarr has a helper, _checkAccessToResource(), that answers: “May this user act on this specific company?”

The customer-portal finding is where the absence of that check becomes exploitable.

The Guard Exists on Read, but Not on Write

The route that reads a company's portal accounts calls the helper and returns 403 to a caller who is not allowed.

The route that writes a new portal password does not call it.




A low-privilege user can therefore:

  1. Set the portal password on a company they are forbidden to even read.

  2. Open the customer portal.

  3. Sign in as that company.

  4. Read that company's invoices, including amounts, due dates, and paid status.

That is the same account the user was refused read access to one request earlier.

The same key gets:

  • 403 on the read

  • 200 on the write

  • Against the same object

  • One request apart

That single fact is the finding.

Why This Is Broken Object Level Authorization

This is broken object level authorization, the flaw the industry also calls IDOR.

The OWASP API Security Top 10 ranks it near the top of its list.

It is CWE-639 precisely because the vulnerable statement selects the object directly from the attacker-supplied key, with no ownership reconciliation afterward.

The same request also leaks a little extra: its response returns the account's pre-update password verifier and login to a caller who was never allowed to read them.

Payment Redirection: The Same Gap, Higher Impact

The payment-redirection finding is the same missing guard on a different object, and it is where the story gets expensive.

The routes that change a company's bank account check only the coarse “create company” right.

They skip both:

  • The per-company authorization check.

  • Dolibarr's dedicated “edit payment information” right, which the web UI does enforce.

So a user can rewrite the IBAN of a supplier they cannot read, replacing it with an account they control.

On its own, that looks like a field edit.

Where that value goes next is the section below.

The Write Route Also Becomes an Oracle

The route leaks a one-bit oracle along the way:

  • A wrong-company write returns 403.

  • A right-company write returns 200.

That lets the caller map which bank-account row belongs to which third party without ever being granted read access.

Both routes had a correct sibling sitting a few hundred lines away in the same file.

The check was written once, on the read path, and never repeated on the write path.

That is the pattern a review of the whole REST surface exists to catch.

It is the kind of authorization gap a white-box pentest reaches by reasoning across roles at the code level rather than probing blindly from outside.

Incorrect Authorization: When the API Checks the Wrong Permission

The third face is the quietest because a check does run.

It is simply keyed to the wrong permission.

When a customer pays an invoice, Dolibarr records the payment, and that record is what marks the invoice settled.

The payment-deletion route guards deletion with the “delete invoices” permission.

Every other payment operation in the codebase requires the dedicated “manage payments” right instead.




These are two different permissions handed to two different roles.

A user granted only the “Delete invoices” right can therefore erase real recorded payments.

What Happens When a Payment Is Deleted

Because an invoice's paid figure is the sum of its payment records, deleting one raises the balance the customer appears to still owe.

In the reproduction:

Invoice

Paid before deletion

Paid after deletion

Amount shown as outstanding

Invoice 1

110

0

110

Invoice 2

220

0

220

Invoice 3

330

0

330

Total

660

0

660

That creates 660 of invented debt across customers who had already paid.

One deleted payment had already been flagged as exported to the accounting books. It was deleted anyway, leaving ledger entries that point to a payment the ERP no longer knows about.

Why a Permission Check Is Not Enough

This is CWE-863, incorrect authorization.

The distinction from a plain missing check matters.

The boundary exists and fires. An unentitled caller receives a clean 403.

It is simply guarding the wrong thing.

That is exactly the sort of bug a scanner keyed to “is there an authorization call?” walks straight past.

There is a smaller flaw riding along as well.

A dead comparison branch runs the deletion routine twice on the success path, firing the delete trigger twice. The default audit log is off, so nothing is recorded at all.

That second bug only surfaces when someone reads the method line by line instead of pattern-matching the signature.

Three Faces, One Lesson

Read the three faces together and the lesson is not about any one module.

A permission model is only as strong as its least-guarded route, and the API surface is where those least-guarded routes hide.

Reflected XSS: From One Admin Click to Account Takeover

Eight findings are access control. The ninth is the reflected XSS, and it earns its own section for two reasons:

  • It is the only Critical in the set.

  • It demonstrates a completely different technique: session riding.

One Shared Template Creates the Entry Point

Almost every Dolibarr module lets an administrator add custom “extra fields,” and every one of those setup pages draws its creation form from a single shared template.

That template takes the type value from the web address and drops it into an inline script. The value is cleaned by a filter that strips HTML but leaves the single quote alone.

// htdocs/core/tpl/admin_extrafields_add.tpl.php:153
init_typeoffields('<?php echo GETPOST('type', 'alpha');

// htdocs/core/tpl/admin_extrafields_add.tpl.php:153
init_typeoffields('<?php echo GETPOST('type', 'alpha');

// htdocs/core/tpl/admin_extrafields_add.tpl.php:153
init_typeoffields('<?php echo GETPOST('type', 'alpha');

A single quote closes the JavaScript string, allowing attacker-controlled code to follow it into the page.

There is also no Content-Security-Policy header to stop the script from running, because Dolibarr leaves that header empty by default.

The alpha filter is an input filter, not an output encoder.

That distinction is the whole bug.

How One Admin Click Becomes Account Takeover

The page is admin-only, so an attacker cannot simply reach it directly.

Instead, they send a logged-in administrator a link.

The moment the administrator opens it, the injected script runs inside the administrator's own session.

The request needs only the create action and the payload. That action sits on an allow-list that skips the CSRF check even at the strictest setting.

The Script Does Not Need the Session Cookie

Stealing the session cookie does not work.

The cookie is marked http-only, and reading it returned empty.

So the script does something more useful.

It:

  • Reads the page's own anti-forgery token from the HTML, where Dolibarr prints it into a meta tag.

  • Uses that token to submit the normal “create user” form same-origin.

  • Creates a fresh administrator with a password and API key chosen by the attacker.

The result is a complete persistence path:

One click by one administrator → attacker-controlled administrator account → permanent administrator login + API access

Those credentials survive the browser closing.

Why the Rogue Account Is Quiet

The new account carries zero permission rows, while the first administrator has 125.

In Dolibarr, the admin flag alone satisfies the guards that matter.

That makes the account both powerful and quiet.

And the vulnerable template is not isolated:

  • It is included by 80 files.

  • The reflection was confirmed on three separate modules.

  • Disabling one module therefore does not close the vulnerability.

Fix and Classification

The fix is an output encoder at that echo, which Dolibarr already ships as dol_escape_js(), plus a real CSP.

The finding maps to CWE-79 and OWASP A03: Injection.

The full chain is in the write-up.

Why the XSS Belongs in the Same Test

What ties this finding back to the eight access-control bugs is not the technique.

It is the destination.

This finding and the members finding arrive at the same place: a rogue administrator, from opposite ends of the application.

A test that only touched the API would have found one.

A test that only touched the front end would have found the other.

The attack surface does not respect that division, so the testing cannot either.

API Data Exposure: Reading Sensitive Data Without Access

Two findings never change a thing on the server.

They are worth pausing on because they are exactly the kind of issues a change-focused test can miss completely.

They are pure reads, and they return data the application is configured to withhold from the caller.

The Filter Oracle

The filter oracle is the more elegant of the two.

The user-list API accepts a sqlfilters search parameter that gets spliced into the query.

It is cleaned enough to block outright SQL injection, but nothing limits which columns you may filter on.

The list response strips sensitive columns—salary and the stored password hash—before it goes out.

But you can still filter by them.

Ask for users where salary is greater than one hundred thousand:

  • A returned row means yes.

  • An empty list means no.

(t.salary:>:234566)  ->  [{"id":"3"}]      # true
(t.salary:>:234567)  ->  []                # false
(t.salary:=:234567)  ->  [{"id":"3"}]

(t.salary:>:234566)  ->  [{"id":"3"}]      # true
(t.salary:>:234567)  ->  []                # false
(t.salary:=:234567)  ->  [{"id":"3"}]

(t.salary:>:234566)  ->  [{"id":"3"}]      # true
(t.salary:>:234567)  ->  []                # false
(t.salary:=:234567)  ->  [{"id":"3"}]

Repeat that with a binary search, and you recover the exact salary one comparison at a time, roughly two dozen requests per figure.

Point the same trick at the password-hash column with a LIKE walk, and you recover a case-folded image of the bcrypt verifier.

None of it ever appears in a response body.

A Second Oracle Exposes the Schema

There is a cheaper second oracle on the same endpoint.

Naming a column that does not exist returns a 503 carrying the raw database error:

"Unknown column"

That enumerates the real column names for you.

Dropping the row filter entirely turns the whole thing into a workforce-wide sweep, partitioning every user by pay band in one request.

Keep the Impact Ceiling Accurate

Worth stating plainly for accuracy: this is not account takeover, and the write-up says so.

Dolibarr's source encrypts the stored API key at rest and rejects a stored ciphertext as a credential, but that defense was not tested.

On the tested instance, the api_key column held literal plaintext.

No key value was extracted and no account was taken over.

Reporting the ceiling of an impact honestly, rather than inflating it, is part of what makes a finding trustworthy.

This is CWE-863 again, because no query structure breaks.

The failure is that no authorization decision is ever made about the columns named, even though the same class already decides, correctly, which columns to strip from the response.

Hash Disclosure: When One Cleaner Forgets a Secret

The hash-disclosure finding is blunter, and it came from the plainest research move in the set.

Every API response runs through a “cleaner” that strips secret fields first.

The users API strips the stored password hash correctly.

The members API cleaner forgot to.




So a user with nothing but “read members” receives every member's live bcrypt verifier—from the list route, all of them at once.

It was confirmed to be the real stored hash, not a placeholder, by:

  1. Matching it against the database.

  2. Checking that it validates the member's actual password.

Why the Exposure Can Reach Further

The exposure reaches beyond the Members module.

When a back-office login is provisioned from a member record, the member's hash is copied into that user's password at creation time.

Where such a user exists and neither password has changed since, the leaked member verifier is also a staff login's verifier.

  • The finding is CWE-522, insufficiently protected credentials.

  • The fix is one line the neighboring class already had.

That finding came from lining up two cleaners side by side and noticing that one did a job its neighbor skipped.

It is the cheapest move in security research, and one of the most productive.

And it only works if you are reading the code rather than scanning it.

Why Exploit Impact Matters More Than the Missing Check

Come back to the payment-redirection bug. It is the clearest argument in this entire set for why proof of impact is the finding, while the missing check is only the starting point.

A changed IBAN is easy to shrug off in triage. It is one field on one record, and a reviewer looking at the diff sees a company's bank details edited by someone who probably had a reason.

The severity looks contained, and the instinct is to file it and move on. So the research did not stop at the edit. It followed the value.

The Difference Between a Changed Field and a Real Attack

Dolibarr can generate SEPA files, the standard instructions a business hands its bank to pay suppliers. The generator takes each supplier's default bank account as the creditor.

The research rewrote three supplier IBANs to attacker-controlled accounts and regenerated the payment batch. All three attacker accounts appeared as the creditors in a real generated file for €6,600.

And the rest of the payment run remained unchanged:

Payment-run detail

Result

Transaction count

Identical to the legitimate run

Control sum

Identical to the legitimate run

Supplier names

Identical to the legitimate run

Invoice references

Identical to the legitimate run

Creditor account

Changed to attacker-controlled accounts

Bank code

Changed

That is the distance between:

“A field can be edited without permission.”

and:

“An attacker's account is in your payment run, and the approver sees exactly what they expect.”

The finding was never the missing authorization call.

It was the SEPA file.

The Same Principle Applies to Payroll

The payroll finding has the same shape.

A tampered salary is a curiosity right up until you watch it surface in the finance export that people actually act on.

The corruption is only interesting where it lands.

That is the whole reason a real test carries a finding downstream instead of stopping at the code.

Why Exploit-Based Testing Changes the Finding

This is precisely what an exploit-based pentest is built to do.

It is the line CodeAnt draws on its own homepage: agents chain findings into working exploit paths, so severity scores stop being the thing you argue about.

Nothing reaches the report unless it was exploited, and it arrives with:

  • The request

  • The response that proved it

  • The steps to reproduce

A generated payment file with the wrong creditor in it is not a number on a scale.

It is a reproduction.

Mapping the Findings to the OWASP API Security Top 10

Eight of the nine are API findings, and they land on a small number of categories in the OWASP API Security Top 10. The clustering is the point. This is not a scattering of unrelated bugs. It is the same two or three authorization failures, over and over.

Finding

Primary OWASP API category

Portal takeover

API1:2023 Broken Object Level Authorization

Payment redirection

API1:2023 Broken Object Level Authorization

Members takeover

API3:2023 Broken Object Property Level Authorization

Payroll tampering

API3:2023 Broken Object Property Level Authorization

Hash disclosure

API3:2023 Broken Object Property Level Authorization

Filter oracle

API3:2023 Broken Object Property Level Authorization

Payment deletion

API5:2023 Broken Function Level Authorization

Self-approval

API5:2023 Broken Function Level Authorization

The reflected XSS sits outside the API list, on A03:2021 Injection in the web Top 10, which is why it was the one finding a scanner reliably catches.

Object-level and function-level authorization, API1 and API5, are about whether you may touch a thing or call an operation. Property-level authorization, API3, is about which fields of a thing you may read or write.

Every mass-assignment finding here is API3, because the request decided which properties got written. Every missing-per-object-check finding is API1, because the request decided which object got reached. That taxonomy is not academic. It tells a reviewer exactly which guard is missing and where it belongs.

How to Fix Common API Authorization Vulnerabilities

The remediations are as instructive as the bugs, because eight of nine share the same two or three repairs. If you write or review REST APIs, this is the section to keep.

1. Use Explicit Allow-Lists for Writable Fields

The first repair is the allow-list.

Every mass-assignment finding here traces to one shape: a blind loop that copies the whole request body onto a record.

The fix is not a longer deny-list of forbidden field names. The next sensitive column added to the model will not be on it.

Instead, use an explicit allow-list of the fields a given endpoint may set:

  • Define exactly which fields each endpoint is allowed to modify.

  • Use a smaller set for self-service routes than for administrative ones.

  • Reject credential and workflow fields outright.

The principle is simple: an endpoint should only be able to write the fields it was explicitly designed to accept.

2. Make Per-Object Authorization Structural

The second repair is making per-object authorization structural.

The portal and bank-account findings existed because one route called the ownership helper and its sibling did not. That is a mistake every author has to remember not to make.

Instead of relying on every individual handler to remember the check, move that authorization into the API base class.

For a nested route under a company ID, the base class should:

  1. Resolve the owning company.

  2. Authorize the request against that company.

  3. Only then run the handler body.

This changes the default behavior:

A new write route fails closed by default, and nobody has to remember anything.

That is a much safer design than relying on developers to consistently add an ownership check to every new endpoint.

3. Use One Authorization Model Across the UI and API

The third repair is one authorization model, shared by the web UI and the API.

Most of these findings are a gap between what the interface enforces and what the API enforces for the same object.

A table-driven map from object action to required right, consulted by both layers, stops the two from drifting apart the way they did here.

It also gives you a single place to audit.

Object action

Required right

Web UI

API

Create

Defined permission

Same authorization model

Same authorization model

Update

Defined permission

Same authorization model

Same authorization model

Delete

Defined permission

Same authorization model

Same authorization model

Read

Defined permission

Same authorization model

Same authorization model

The important part is not the specific permission names. It is that both layers consult the same authorization mapping, rather than maintaining separate rules that can drift apart.

4. Automate Credential-Exposure Checks

The credential-exposure findings add one more repair that is cheap and worth automating.

A test that serializes a populated object through each response cleaner, and fails the build if any field matching a password or key pattern survives, would have caught the members hash leak before it shipped.

This turns a class of response-exposure bugs into something that can be caught automatically during the build rather than discovered after deployment.

Interim Workarounds for Teams Running Dolibarr Today

There is an operator side too, for teams running Dolibarr today rather than patching it.

Every write-up carries interim workarounds that need no code change.

Reduce Unnecessary Privileges

Revoke the routine rights:

  • adherent.creer

  • societe.creer

  • The invoice-delete right

Remove them from accounts that do not strictly need them, and re-issue the API keys that held them.

Restrict or Disable the REST API

  • Where the REST module is unused, disable it.

  • Where it is used for one integration, restrict key issuance to that account.

Monitor the Money Paths

For the money paths specifically:

  • Reconcile creditor details in every SEPA file before release.

  • Watch the bank-account and payment tables for changes.

None of that fixes the bug.

All of it shrinks the window.

The Common Pattern: Taken together, these repairs describe an API that fails closed instead of open. That is the property every one of these nine findings was missing.

What These 9 Findings Reveal About API Penetration Testing

Step back from the bugs and look at the moves that produced them, because those moves are the actual substance of a penetration test.

1. Trace Input Into Execution

The first move was reading input into a script and watching what executes.

The extra-fields template dropped a URL parameter into JavaScript with only a light filter. The obvious objection was that the page is admin-only.

The research pushed past that objection.

An admin-only reflected XSS is still a real bug if you can get an admin to open the link, and that is ordinary phishing.

2. Compare UI and API Authorization

The second move was diffing the web UI's permission check against the API's for the same object.

That is where the following findings came from:

  • Members password reset

  • Payroll write

  • Expense self-approval

The UI guarded each one. The API did not.

The only way to see the discrepancy was to open both and compare how they enforced the same operation.

3. Test Read and Write Paths Separately

The third move was treating read routes and write routes as separate doors.

The portal read route was already correct, so it would have been easy to call the area clean.

Checking the write route separately is what surfaced the takeover.

Enforcement on the read path proves nothing about the write path.

4. Follow Tampered Values Downstream

The fourth move was following a tampered value downstream.

A changed IBAN is easy to dismiss in isolation. The research instead chased it into the SEPA file, where the attacker's account was sitting as a creditor in a real payment run.

The vulnerability became significant because the testing followed the manipulated value to its actual business consequence.

5. Compare Similar Code Paths

The fifth move was lining up two pieces of the same codebase that should behave alike.

Two response cleaners were supposed to perform the same kind of filtering. One stripped the password hash; the other forgot to.

That single missing line resulted in a bulk credential leak.

Why Code-Aware Testing Matters

None of these techniques is exotic.

What they share is that every one depends on reading and reasoning about the code, not simply probing the running application from the outside and hoping.

A black-box scan of Dolibarr's API would see a lot of authenticated endpoints returning 200. It would have no way to know that a 200 on one route, from a key that gets a 403 on its sibling, represents a broken authorization boundary.

This is the case for code-aware offensive testing.

When the agents can read the source, they can identify that:

  • A write route selects an object directly from an attacker-supplied key.

  • A mass-assignment loop reaches a credential column.

  • One response cleaner is missing a line that its neighboring cleaner has.

They can then run those specific paths against the live application to prove them, using black-, white-, and grey-box testing, and re-attack with what they learned from the code.

The grey-box step is what reaches the authorization and business-logic flaws that make up most of this set.

One Code Intelligence Layer, Two Security Functions

The defensive half of the same platform is the other end of these findings.

Patterns such as:

  • Mass assignment

  • A missing per-object authorization check

  • A foreach that copies a request body onto a record

  • A response cleaner that forgot a field

are patterns that AI code review can flag in the pull request, before the route ever ships.

The same code intelligence that catches the pattern at commit time feeds the offensive engine that confirms which patterns are genuinely exploitable in production.

That is what “defensive and offensive on one platform” means in practice: these nine findings show what it looks like when both halves work from the same understanding of the code.

Conclusion: Why API Pentesting Must Prove Exploitability

Nine vulnerabilities. One application. And a pattern that a scanner alone cannot fully explain. That is the difference between finding a weakness and proving an attack.

CodeAnt AI Security Research found these issues by tracing how Dolibarr's permissions, API routes, objects, and data flows actually behaved, then validating the highest-risk paths against a running instance. That is why effective API penetration testing cannot stop at identifying suspicious code or missing authorization checks. The critical step is proving what an attacker can actually reach, change, access, or control.

And that is where the nine findings become more than nine vulnerabilities. They show how small authorization gaps can become serious attack paths when you follow them through the application.

The most important question in API security is not "How severe is this vulnerability?" It is "What happens when someone exploits it?"

If you want the same reasoning run against your own code and attack surface, start with a free scan on the CodeAnt pentest platform and read the finding it returns, with the request and response that proved it.

Then work through the write-ups above for the full reproductions.

FAQs

How does API penetration testing find authorization vulnerabilities?

What should be tested during an API penetration test?

Why is exploitability important in API security testing?

What is the difference between API security scanning and API penetration testing?

How can developers prevent broken access control in REST APIs?

Start Your 14-Day Free Trial

AI code reviews, security and quality trusted by modern engineering teams.

Table of Content
No headings found on page
Ship clean & secure code faster

Get Pentest Report

NO CC REQUIRED