CodeAnt AI Security Research
CVE-2026-71510: Dolibarr API Filter Lets Low-Privilege Users Query Hidden Data

Amartya Jha
CEO, CodeAnt AI
TL;DR
A Dolibarr REST API caller holding nothing more than the list-users right can read every account's stored salary, hourly cost, and daily cost, plus a case-folded copy of the bcrypt password verifier, none of which that caller may see in a normal response. Each request returns a single true or false bit from an endpoint that otherwise refuses to serialize those fields at all. The behavior is fixed in Dolibarr 24.0.0.
What is CVE-2026-71510?
CVE-2026-71510 is an incorrect-authorization flaw (CWE-863) in the Dolibarr ERP CRM REST API, present through 25.0.0-alpha (develop, commit ab7e60406d4702e943f62b96c344439f017f6c49) and fixed in 24.0.0.
The GET /api/index.php/users endpoint splices a caller-supplied sqlfilters expression into its SQL WHERE clause without checking whether the caller may reference the columns it names. That turns any confidential column, salary, salaryextra, thm, tjm, pass_crypted, into a blind boolean oracle for a caller who holds only the user->user->lire (list users) permission.
Its severity is 6.5 (Medium), vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N, and its impact is disclosure only, not account takeover.
Dolibarr Already Knows These Fields Are Sensitive
The correct authorization decision is not missing from the codebase. It runs on every response.
In htdocs/user/class/api_users.class.php, the serializer _cleanObjectDatas() (lines 1289-1336) unconditionally scrubs the credential family and gates the payroll fields on the caller's rights:
Line 1328 is the whole point. The class knows who may see a salary, because it computed $canreadsalary and used it to strip four columns from the outgoing JSON.
This is the same split-brain authorization shape we catalog across Dolibarr's REST layer. The right decision exists, but it runs on only one of the two surfaces that touch the row. It runs on the way out, never on the way in, on the sqlfilters operand the query is built from a few dozen lines earlier.
Why CVE-2026-71510 Is Not SQL Injection
Users::index() checks one permission, then splices the filter in:
Line 88 authorizes listing users. Nothing between it and line 116 authorizes the columns the filter names. The build happens in dolForgeSQLCriteriaCallback(), htdocs/core/lib/functions.lib.php:
The regex at 16306 is the reason this is not SQL injection. The class [^a-z0-9\._] deletes every character an injection needs: quotes, spaces, parentheses, dashes, hashes, semicolons, asterisks, equals signs.
What survives is exactly [a-z0-9._], the alphabet of a table-qualified column name and nothing more. The value on the other side is single-quoted and run through $db->escape().
So the statement that executes is the well-formed shape the developer drew, WHERE (t.<name> <OP> '<value>'). No predicate appended, no boolean logic rewritten, no quote broken out of, no second statement stacked, no comment closing the tail. The parser sees a column, an operator, a literal, exactly what it is meant to see.
The attacker's only freedom is which column name lands in the <name> slot, and t.salary is not malformed SQL. It is a perfectly legal column reference this caller must not be permitted to name.
That distinction is the whole diagnosis. SQL injection (CWE-89) is a failure to neutralize special elements, and here the regex already deleted every special element there was. What is missing is the decision of whether this principal may reference this column, an authorization decision, so the weakness is CWE-863.
Two symptoms confirm it. The WHERE clause evaluates the confidential column correctly and returns the right rows, where a corrupted query would throw a parse error or need a UNION, and this one is valid every time. Meanwhile the serializer strips that same column from the body, because the program had already ruled the value confidential, so a valid query returning it is an authorization gap and not an injection.
Even the failure mode stays well-formed. An unknown column draws HTTP 503 with the driver's Unknown column 't.x' in 'WHERE', a syntactically perfect query the database rejects on semantics, and a free schema map before a single value is read.
How sqlfilters Turns Hidden Fields Into a Blind Oracle
Every request below ran against an isolated Dolibarr container on laptop loopback. No hosted or third-party instance was ever touched. The attacking key holds exactly user->user->lire and user->user->creer, with admin=0, an ordinary can-list-users account.
First, confirm the endpoint really does hide the fields. Fetching the victim by id returns a body with salary, salaryextra, thm, tjm and pass_crypted all absent. The endpoint's stated position is that this key may not see them.
Now move the same column into sqlfilters and send three requests, all HTTP 200:
A returned row means the predicate held. An empty array means it did not. That single bit pins the salary to 234567 in three requests.
Wrap it in a binary search over 0 to 16,777,216 and each numeric column falls in 24 probes. salary, salaryextra, thm and tjm came back as 234567 / 765432 / 2345 / 5432, matching the database exactly.
Swap the operator for like and walk the string one character at a time. 1,286 probes recover the 60-character pass_crypted verifier, case-folded because MariaDB's default utf8mb4_uca1400_ai_ci collation makes LIKE case- and accent-insensitive.
The full run is 1,382 requests, every one an ordinary authenticated GET. Drop user_ids and one (t.salary:>:100000) returns everyone above that band, no targeting needed.
Who Can Exploit CVE-2026-71510 and What Can They Read?
user->user->lire is not a privileged grant. It is what you hand an HR assistant, a helpdesk agent who looks people up, or an integration account that syncs users into another system.
Any one of those keys can now read the whole workforce's pay, base salary, extra salary, hourly and daily cost, from an endpoint built to withhold exactly those numbers from exactly those callers.
The ceiling is worth stating precisely. This is a disclosure bug, not a takeover. What was actually recovered is the four payroll integers and a case-folded image of the bcrypt verifier.
That image is not a usable credential. bcrypt needs the exact string, and the lowercased value stays consistent with on the order of 1.8×10¹³ hashes, no shortcut to cracking.
The same oracle answers yes or no questions about admin and api_key, but no key value was extracted and no encryption was tested. On the tested instance api_key simply held literal text. Nobody's account is taken over here. What leaks is confidential data the application had already decided this caller could not read.
How Dolibarr Fixed CVE-2026-71510
Dolibarr 24.0.0 stops trusting the caller to name columns. The durable pattern is the one the class was already half-doing, which is to run the serializer's authorization decision on the inputs, not only the outputs.
Concretely, allowlist the operands sqlfilters may reference before the splice at line 116. Reuse the $canreadsalary expression from line 1328 to reject t.salary, t.salaryextra, t.thm and t.tjm when the caller lacks salaries->read. Make the never-returned credential list illegal as operands for every principal, admins included. And stop handing $this->db->lasterror() back in the 503 body so the schema oracle goes quiet.
The broader lesson is that a filter which guarantees your SQL parses is not an access control. Syntactic integrity and column authorization are different jobs, and passing the first tells you nothing about the second.
Since the identical splice appears across 44 REST classes, the decision belongs in the shared Api::_checkFilters() hook, where operand authorization becomes structural instead of re-derived per endpoint.
The Filter Was Secure. The Authorization Wasn't
CVE-2026-71510 shows why sanitizing a filter is not the same as securing it. Dolibarr correctly strips SQL metacharacters and produces valid queries, but it never asks whether the caller is allowed to name the column inside that query. That turns sqlfilters into a blind oracle for salary and other hidden fields.
CodeAnt AI Security Research found this as one of nine Dolibarr findings tied to the same split-brain authorization pattern: the application knows a field is sensitive on one path but fails to enforce that decision on another. The finding required proving that a low-privilege user could recover data the API deliberately refused to return, not merely identifying suspicious SQL construction.
The takeaway is simple: sanitizing what a user can write into a query does not determine what they are allowed to read. If your APIs expose filter, search, sort, or query parameters, authorize the fields they can reference, not just the syntax they can use.
This research is part of an ongoing effort by CodeAnt AI Security Research to audit widely-used open-source packages for security vulnerabilities. We believe the open-source ecosystem deserves better tools, better auditing, and more support for the maintainers who keep it running. More findings will be published as patches ship and coordinated disclosure timelines are met.
If you are a maintainer and have been contacted by our team, thank you for your work. If you believe your package may be affected by a similar pattern, we’d love to help: securityresearch@codeant.ai
Related reading: This finding is one node in the split-brain authorization pattern, the pillar behind all nine findings, where one filter can pass and still leak. A closer cousin is CVE-2026-71506, where the check also runs and still lands on the wrong answer, because the payments API validates the wrong permission. Both are CWE-863, a control that executes but does not protect what it should.
FAQs
Is CVE-2026-71510 SQL injection?
What is a blind boolean oracle, and how does it leak data in CVE-2026-71510?
Can the password hash leaked by CVE-2026-71510 be used to log in?
What privileges does an attacker need to exploit CVE-2026-71510?
Which Dolibarr versions are affected, and how is CVE-2026-71510 fixed?
Why does sqlfilters bypass the protection on hidden user fields?

Get Pentest Report
