Feb 26, 2026·6 min read·79 visits
Unauthenticated attackers can send a crafted HTTP POST request to Craft CMS versions 4.0.0 through 4.4.14 to execute arbitrary PHP code. This exploits a flaw in how the CMS handles user-supplied configuration arrays, turning a standard feature into a remote shell generator.
A critical remote code execution vulnerability in Craft CMS allows unauthenticated attackers to hijack the application's object instantiation logic. By manipulating the `ConditionsController`, attackers can trick the underlying Yii2 framework into creating arbitrary PHP objects with malicious configurations, leading to immediate server compromise via Guzzle or Imagick gadgets.
Craft CMS is the darling of the agency world. It’s slick, it’s flexible, and it’s built on top of the Yii2 framework. For the uninitiated, Yii2 is a powerful beast that relies heavily on "magic"—specifically, a dependency injection container that can instantiate classes and configure their properties on the fly using arrays. It’s a developer’s dream: define a class, throw an array of settings at it, and the framework handles the rest.
But here’s the thing about magic: if you let the wrong person wave the wand, things explode. In September 2023, researchers discovered that Craft CMS was a little too generous with who held the wand. Specifically, the ConditionsController allowed unauthenticated users to pass arbitrary configuration arrays directly into this object creation engine.
This isn't your standard buffer overflow or a forgotten SQL injection. This is Object Injection via Configuration. It’s the architectural equivalent of letting a stranger walk into a car factory, hand the foreman a blueprint for a tank, and having the factory build it without asking questions. The result? A CVSS 10.0 vulnerability that allows anyone on the internet to execute code on your server as www-data.
The vulnerability lives in craft\controllers\ConditionsController.php. This controller is designed to handle dynamic conditions for content filtering. To do this, it needs to accept configuration data from the user to know what conditions to apply. So far, so standard.
The fatal mistake was in the beforeAction method. In Yii2, beforeAction runs before the main controller action. Craft CMS developers decided to process the request parameters here. They took the raw POST data, specifically a parameter named config, decoded it from JSON, and then—here is the kicker—passed it straight into the object creation service.
Because Yii2’s BaseObject allows for "Mass Assignment" (setting properties based on array keys), an attacker can control every public property of the class they ask to create. Even worse, Yii2 supports "behaviors" (keys starting with as ) and "events" (keys starting with on ). This means an attacker isn't just setting simple strings or integers; they can attach entire behavior classes or define event handlers that execute arbitrary PHP code.
Let's look at the crime scene. In the vulnerable versions (<= 4.4.14), the code looked something like this:
// Vulnerable code in ConditionsController
public function beforeAction($action)
{
// ... various checks ...
// 1. Take user input blindly
$config = $this->request->getBodyParam('config');
// 2. If it looks like JSON, decode it
if (is_string($config) && Json::isJson($config)) {
$config = Json::decode($config);
}
// 3. Create the object using the user's config
$condition = Craft::createObject($config);
// ...
}See the problem? There is zero validation on $config. If I send {"class": "\Evil\Class", "property": "bad_value"}, the application happily creates \Evil\Class.
The fix in version 4.4.15 was a multi-step remediation. First, they ensured the configuration is "cleansed" of dangerous keys like on (events) and as (behaviors). Second, they strictly typed the input. Third, and perhaps most importantly, they enforced authentication checks before this logic runs.
// Patched code
public function beforeAction($action)
{
// Enforce permissions first!
$this->requireCpRequest();
// Sanitize the config
$config = Craft::$app->getRequest()->getBodyParam('config');
// ... decoding logic ...
// Cleanse dangerous keys
$config = Component::cleanseConfig($config);
return parent::beforeAction($action);
}So we can create objects. Great. But how do we get a shell? We need a "gadget"—a class already present in the codebase that does something dangerous. Since Craft CMS includes Guzzle (a popular HTTP client), we have a perfect candidate: \GuzzleHttp\Psr7\FnStream.
This class is a wrapper for PHP streams. It has a magic method, __destruct(), which is called when the object is destroyed (garbage collected) at the end of the script execution. The __destruct method in FnStream checks if a custom close function (_fn_close) is defined and, if so, executes it.
Here is the attack chain:
\GuzzleHttp\Psr7\FnStream object._fn_close to a string representing a PHP function, like phpinfo or system.FnStream object is destroyed.__destruct() fires, calls call_user_func($this->_fn_close), and our code runs.The payload looks remarkably simple for such a devastating exploit. A standard Nuclei template or Python script sends a POST request to index.php with:
{
"action": "conditions/render",
"config": {
"class": "\\GuzzleHttp\\Psr7\\FnStream",
"__construct()": [{"close": null}],
"_fn_close": "phpinfo"
}
}If you see the PHP configuration page in the response, you own the server.
This is a full unauthenticated RCE. The attacker runs as the web server user (usually www-data). From here, the possibilities are endless and terrifying.
First, they can read your .env file, stealing your database credentials, AWS keys, and the CRAFT_SECURITY_KEY. With the database credentials, they can dump your entire user table, including hashed passwords. With the security key, they can forge session cookies or encrypt malicious payloads to be executed elsewhere.
Second, they can modify the CMS itself. They could inject a persistent backdoor into index.php that survives updates, or install a web shell deep in the vendor directory where you're unlikely to look. Ransomware gangs love these vulnerabilities because they are easy to automate. One script can scan thousands of servers, drop a cryptominer or encrypt the filesystem, and move on.
If you are running Craft CMS 4.0.x through 4.4.14, you are vulnerable. Stop reading and update to 4.4.15 or higher immediately. The patch introduces strict validation on the config parameter, stripping out the magic keys that allowed the exploit to work.
After patching, you aren't done. You must assume you were compromised if your server was exposed to the internet.
CRAFT_SECURITY_KEY and any API keys stored in your .env file.index.php containing action=conditions/render or suspicious JSON payloads involving FnStream or Imagick.composer install) to ensure no backdoors were planted in library files.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
Craft CMS Pixel & Tonic | >= 4.0.0-RC1, <= 4.4.14 | 4.4.15 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-94 (Code Injection) |
| CVSS Score | 10.0 (Critical) |
| Attack Vector | Network (HTTP POST) |
| EPSS Score | 0.93942 (99.88%) |
| Exploit Status | Active / Weaponized |
| Gadget Used | GuzzleHttp\Psr7\FnStream |
CVE-2026-115895 is a high-severity code injection vulnerability (CWE-94) in the happy-dom npm package prior to version 21.0.4. The vulnerability exists within the ECMAScriptModuleCompiler component, which transforms ECMAScript module syntax into executable Node.js functions when JavaScript evaluation is enabled. Due to an incomplete fix for CVE-2026-33943, the compiler failed to validate local named import aliases against legal JavaScript identifier rules. Attackers supplying crafted module imports—specifically utilizing destructuring default initializers alongside JSON module attributes—can inject arbitrary JavaScript expressions directly into the host Node.js runtime context.
An improper input validation and authorization bypass vulnerability exists in the LangGraph storage layer affecting langgraph, langgraph-checkpoint-postgres, and langgraph-checkpoint-sqlite. Because relational store implementations serialize namespace tuples into dot-delimited strings without validating labels on read, search, delete, and batch operations, an attacker who controls a namespace label can inject periods (.) to traverse namespace boundaries and access, modify, or delete data belonging to other namespaces.
CVE-2026-115886 (GHSA-2wqp-q3c6-ccj5) is a Server-Side Request Forgery (SSRF) and Unsafe Reflection vulnerability in MLflow versions 3.12.0rc0 prior to 3.17.0. Unauthenticated REST API endpoints accept serialized third-party scorer JSON payloads that are dynamically deserialized and reflectively instantiated without sanitizing constructor keyword arguments. When optional dependencies such as trulens and trulens-providers-litellm are installed, attackers can inject egress-steering arguments into the scorer payload, forcing the MLflow server to dispatch outbound HTTP requests to arbitrary internal destinations or cloud metadata services.
CVE-2026-113872 (GHSA-q7qp-rjg7-q26m) is a high-severity denial-of-service vulnerability affecting the multer multipart/form-data upload middleware for Node.js and Express in versions 2.0.0 through 2.4.0. When an endpoint is configured to accept multiple files using an asynchronous fileFilter callback, a crafted, truncated multipart HTTP request can trigger an unhandled error event on the underlying busboy stream parser after request teardown has already completed. Because all error listeners are stripped during teardown, Node.js treats the resulting error event as an uncaught exception and immediately terminates the server process.
CVE-2026-115897 (also tracked as GHSA-m5mm-p8h7-m29v and aliased as CVE-2026-115754) is a Cross-Site Scripting (XSS) vulnerability in the Yahoo serialize-javascript npm package prior to version 7.1.3. The flaw exists in the regular expression used by escapeFunctionBody() to neutralize HTML script closing tags within serialized JavaScript functions. Because function bodies are sanitized in isolation before structural formatting is applied, a function whose string representation terminates directly with the sequence '</script' bypasses the positive lookahead check. When combined with serialization formatting options that introduce whitespace delimiters and a subsequent property that supplies a closing bracket, an attacker can prematurely terminate an enclosing HTML <script> block and inject arbitrary HTML or JavaScript into the rendered document.
CVE-2026-109841 is a Subresource Integrity (SRI) verification bypass vulnerability in undici, the official HTTP/1.1 and HTTP/2 client library for Node.js that powers the global fetch() implementation. The flaw stems from improper handling of case sensitivity (CWE-178) when validating cryptographic hash algorithm tokens within SRI metadata. Because undici compared incoming algorithm identifiers against a strictly lowercase set without prior ASCII-lowercase normalization, valid uppercase or mixed-case tokens such as SHA256- or Sha384- were treated as unrecognized algorithms and discarded. Under SRI forward-compatibility rules, discarding all provided tokens results in an empty integrity list, causing undici to fail open and accept tampered HTTP response bodies without cryptographic verification.