TL;DR:EspoCRM's built-in formula scripting engine bypasses internal field-level ACL restrictions, allowing an admin to overwrite a path-traversal-sensitive field on file attachments. Combined with an unsanitized file path in the upload storage layer, this yields arbitrary file read, arbitrary file write, and — with a small .htaccess trick — full remote code execution aswww-data. EspoCRMis one of those projects that occupies an interesting niche. It is open-source, PHP-based, sits at around 3,000 GitHub stars, and gets deployed by small-to-midsize organizations who need a CRM but do not want to pay Salesforce prices. It handles contacts, leads, sales pipelines, support cases, emails — the usual. It also has a surprisingly deep feature set for an open-source tool, including workflow automation, a BPM engine, and something called theformula scripting engine. I recently found myself auditing EspoCRM v9.3.3 as part of my independent vulnerability research. My target was the official Docker image (espocrm/espocrm), which at the time shipped v9.3.3 and runs Apache aswww-data. If you are testing against a different deployment — a manual install, a different base image, or a non-Debian host — the web server process may run under a different OS user, but the vulnerability chain is the same. Much of the application is well-structured — PHP with a clean service layer, proper ACL enforcement on most API endpoints, entity-level and field-level access controls defined in JSON metadata files. Nothing jumped out immediately from the usual quick wins: no obvious SQL injection, no deserialization of user input, noeval()sitting in a controller. But then I found the formula engine. EspoCRM ships with a built-in scripting language designed for business logic automation. Admins use it to define calculated fields, workflow actions, and BPM process steps. The syntax looks like a simplified functional language: Crucially, the formula engine is also exposed via a REST endpoint for testing and ad-hoc execution: This is admin-only, which is fair. An admin should be able to test formulas. The engine provides functions likerecord\update(entityType, id, field, value)to modify any entity in the database, andrecord\attribute(entityType, id, field)to read any field from any entity. These are powerful primitives, and the implicit trust model is that an admin can do anything. I was initially inclined to agree. An admincando almost anything through the normal UI. But “almost” is doing a lot of work in that sentence. One of the first things I do when auditing a PHP application with role-based access control is to compare how the same operation is handled across different code paths. If a field can be updated via the APIandvia some internal mechanism, do both paths enforce the same restrictions? The normal API path for updating an entity runs throughRecord\Service::filterInput(). This method calls$this->acl->getScopeForbiddenAttributeList()to retrieve the list of fields marked asreadOnly,internal, orforbiddenin the entity's ACL metadata. It strips those fields from the input before the save. This is the right thing to do. The formula engine'srecord\updatefunction, however, takes a different path. Inapplication/Espo/Core/Formula/Functions/RecordGroup/UpdateType.php, aroundline 79: Three lines. Set the data, run a check, save. The problem is whatEntityUtil::checkUpdateAccess()actually checks: it only prevents changing thetypefield onUserentities tosuper_adminorsystem. That is theentireguard. There is no call togetScopeForbiddenAttributeList(). There is no check against the entityAcl metadata. Fields markedreadOnlyin the ACL layer — fields that the normal API would silently strip — are writable through the formula engine. The EspoCRM maintainer considers the formula engine’s lack of field-level ACL enforcement to be intentional, not a vulnerability. His position:“Formula engine does not apply ACL by design.”He also noted that users actively use formula to writesourceIdfor legitimate purposes, and that an admin already has other paths to code execution (e.g., extension uploads). The maintainer edited the advisory to remove much of the technical detail around the formula engine’s role, including thefilterInput()comparison and therecord\attribute/record\createimpacts. In the published advisory, the formula engine is documented as theexploitation path— how the attacker reachessourceId— rather than as a root cause requiring a fix. The primary remediation targetsgetFilePath()only. The fix itself is solid —basename()applied across six path construction sites within 24 hours of the report. My analysis in this section reflects my own assessment of the access control model and the security consequence of the design: areadOnlyfield that the REST API correctly protects is reachable via an alternative code path that doesn’t apply the same restrictions. The advisory tells one version of this story; the technical details in this write-up tell the full version. Rea...
This vulnerability (CVE-2026-33656) is an authenticated remote code execution chain in EspoCRM where an admin user can exploit a formula engine ACL bypass to manipulate a file attachment field, which is then combined with a path traversal flaw in the upload storage layer to achieve arbitrary file write and ultimately RCE as the web server user. The issue affects EspoCRM versions up to and including 9.3.3.