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 |
|---|---|---|---|---|
Cross-site scripting | 9.3 Critical | One admin-opened link creates a second admin with an attacker-chosen password and API key | ||
Mass assignment | 8.3 High | A membership clerk resets the administrator's password and owns the instance | ||
Broken object level authorization | 8.1 High | Sets the portal password of a company it cannot read, signs in, reads its invoices | ||
Incorrect authorization | 6.5 Medium | Deletes recorded customer payments with only the "delete invoices" right | ||
Authorization bypass | 6.5 Medium | Rewrites supplier bank details that then land in the SEPA payment file | ||
Mass assignment | 6.5 Medium | Rewrites salaries and cost rates with no payroll permission at all | ||
Business logic flaw | 6.5 Medium | Approves your own claims into the payable queue, breaking separation of duties | ||
Incorrect authorization | 6.5 Medium | Recovers salaries and a password-hash image through a search-filter oracle | ||
Insufficiently protected credentials | 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 |
|---|---|---|---|
None. A crafted link | One admin opens it | A second administrator with attacker credentials | |
Create-members right, own API key | None | Admin password reset, full instance control | |
Create-companies right, own API key | None | Another company's invoices read | |
Delete-invoices right, own API key | None | Recorded payments erased, phantom debt | |
Create-companies right, own API key | None | Attacker IBANs in the SEPA file | |
Edit-own-profile right, own API key | None | Salary and cost rates rewritten | |
File-expenses right, own API key | None | Claims approved into the payable queue | |
List-users right, own API key | None | Salaries and a hash image read blind | |
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:
Loads the user named by the attacker-supplied ID.
Rewrites that account's password.
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:
Point the request at the administrator.
Set a new password.
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:
200responseAdministrator'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:
Set the portal password on a company they are forbidden to even read.
Open the customer portal.
Sign in as that company.
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:
403on the read200on the writeAgainst 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.
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.
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:
Matching it against the database.
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 |
|---|---|
API1:2023 Broken Object Level Authorization | |
API1:2023 Broken Object Level Authorization | |
API3:2023 Broken Object Property Level Authorization | |
API3:2023 Broken Object Property Level Authorization | |
API3:2023 Broken Object Property Level Authorization | |
API3:2023 Broken Object Property Level Authorization | |
API5:2023 Broken Function Level Authorization | |
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:
Resolve the owning company.
Authorize the request against that company.
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.creersociete.creerThe 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
foreachthat copies a request body onto a recordA 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.


