Craft CMS RCE via Condition Config Bypass
CVE-2026-72778 is an authenticated RCE in Craft CMS control panel where JSON-encoded condition configs bypass sanitization, enabling OS command execution.
Overview
CVE-2026-72778 is an authenticated remote code execution vulnerability affecting Craft CMS versions from 4.0.0-RC1 before 4.18.2 and from 5.0.0-RC1 before 5.10.6. The flaw resides in the control panel’s element-search condition handling pipeline, where a two-stage processing path allows attacker-controlled JSON payloads to survive sanitization and reach Yii’s object instantiation layer carrying dangerous behavioral configuration.
The vulnerability stems from an architectural inconsistency: Craft correctly sanitizes the outer condition array using Component::cleanseConfig(), which strips Yii’s special configuration keys (prefixed with as for behaviors and on for events). However, when Conditions::createCondition() subsequently decodes the condition.config JSON string and merges the resulting array into a new configuration object, it does so without re-running cleanseConfig() on the decoded output. Because the malicious keys are hidden inside a JSON string at the time of the first cleanse, they are invisible to the sanitizer — only materializing as dangerous PHP array keys after decoding, at which point they are passed directly to Yii’s component factory.
Any attacker with a valid authenticated control panel session and a CSRF token can trigger this path by submitting a crafted condition payload to the element-search endpoint. This makes it particularly relevant in multi-user Craft installations — including SaaS platforms, agency-managed sites, and enterprise deployments — where editor or author-level accounts with panel access are broadly distributed. Compromise of a single low-privilege account is sufficient for full server-side code execution.
Technical Analysis
The root cause is a classic double-decode sanitization bypass, well understood in the context of serialization and configuration injection, but manifesting here through Yii’s flexible component configuration DSL.
Yii’s BaseObject::__construct() and Yii::createObject() accept configuration arrays that can include as BehaviorName and on EventName keys to dynamically attach behaviors and event handlers to objects. Craft’s Component::cleanseConfig() exists specifically to strip these keys from user-supplied configuration before object creation. The gap is that it only runs on the top-level decoded array — not on nested values that are themselves serialized structures.
The vulnerable flow looks like this:
// VULNERABLE: Craft sanitizes the outer config array, but condition.config
// is still a raw JSON string at this point — its contents are not inspected.
$conditionData = $request->getBodyParam('condition');
// cleanseConfig strips 'as ...' and 'on ...' keys from the TOP-LEVEL array
$conditionData = Component::cleanseConfig($conditionData);
// Later, createCondition decodes condition.config and merges without re-cleansing:
public static function createCondition(array $config): ConditionInterface
{
if (isset($config['config']) && is_string($config['config'])) {
// JSON decode reveals the hidden Yii behavior/event keys
$innerConfig = Json::decode($config['config']);
// MISSING: Component::cleanseConfig($innerConfig) call here
$config = array_merge($config, $innerConfig);
}
// Yii::createObject receives a config array containing 'as ...' or 'on ...'
return Yii::createObject($config);
}
An attacker crafts a POST body where condition[config] is a JSON string embedding Yii behavior configuration. Because the outer condition array contains only a benign-looking config string key at cleanse time, cleanseConfig() finds nothing to remove. After Json::decode(), the array now contains keys such as "as shell" pointing to a behavior class, or "on beforeValidate" pointing to a closure-equivalent callable. When Yii::createObject() processes this array, it instantiates the specified class and attaches the behavior or event — which can invoke arbitrary PHP and, by extension, OS-level commands via exec(), shell_exec(), proc_open(), or similar.
A representative malicious payload targeting behavior injection:
{
"condition": {
"class": "craft\\elements\\conditions\\ElementCondition",
"config": "{\"as pwned\":{\"class\":\"yii\\\\base\\\\Behavior\",\"events\":{\"init\":\"shell_exec\"}}}"
}
}
The precise gadget chain depends on the Yii version and available classes in the autoloader, but the injection point is consistent across all affected Craft versions. The attack requires only a valid session cookie and CSRF token, both trivially obtained by any authenticated user.
Impact
An attacker exploiting CVE-2026-72778 achieves unauthenticated-equivalent arbitrary code execution relative to the PHP process — typically running as www-data, nginx, or a dedicated web user. From this foothold, practical consequences include:
- Full filesystem read/write at web-process privilege level, exposing
.envfiles, database credentials, API keys, and uploaded user content. - Database exfiltration: With credentials from the environment, an attacker can dump the entire Craft database, including user PII, hashed passwords, and transactional records.
- Lateral movement: In cloud or containerized environments, the web process often has access to instance metadata endpoints (e.g., AWS IMDSv1), enabling credential theft and account takeover.
- Persistent backdoor installation: Writing webshells or modifying Craft templates gives lasting access even after credential rotation.
- Supply chain risk: Sites using Craft for content delivery pipelines may expose downstream consumers or API integrators.
The CVSS 8.8 score reflects the High severity with network attack vector, low complexity, no required privileges beyond authentication (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). The authentication requirement is the sole factor preventing a Critical rating — and in practice, Craft installations routinely grant panel access to content editors, making that bar low.
How to Fix It
Upgrade immediately. Craft has released patched versions that apply cleanseConfig() recursively after decoding condition.config:
# Composer upgrade for Craft 4.x
composer require craftcms/cms:"^4.18.2"
# Composer upgrade for Craft 5.x
composer require craftcms/cms:"^5.10.6"
The correct remediation pattern applies sanitization to the decoded inner config before merging:
// FIXED: Re-run cleanseConfig on the decoded inner configuration
public static function createCondition(array $config): ConditionInterface
{
if (isset($config['config']) && is_string($config['config'])) {
$innerConfig = Json::decode($config['config']);
// Cleanse the decoded structure before it reaches Yii::createObject
$innerConfig = Component::cleanseConfig($innerConfig);
$config = array_merge($config, $innerConfig);
}
return Yii::createObject($config);
}
If an immediate upgrade is not possible, restrict control panel access by IP allowlist at the WAF or web server layer as a temporary compensating control. This does not eliminate the vulnerability but significantly reduces the attack surface by limiting who can reach the endpoint.
Our Take
This vulnerability is a textbook example of sanitization scope mismatch — a class of bug that our research team sees repeatedly across frameworks that use flexible, configuration-driven object instantiation. The developer correctly identified the risk of Yii’s as/on config keys and added a cleansing step; the failure was in not accounting for the full data lifecycle, specifically that a sanitized string can carry unsanitized structured data that only becomes dangerous after a subsequent decode.
The lesson for framework developers is that sanitization must be applied at the point of use, not just at the point of intake. When a value traverses a serialization boundary — JSON encode/decode, base64, URL encoding — it must be re-validated on the other side. Trusting that earlier sanitization “covered” it is a structural assumption the attacker is specifically designed to violate.
For enterprises running Craft at scale, this underscores the risk of treating CMS installations as low-criticality assets. Control panel credentials are a direct path to server compromise, and credential hygiene, MFA enforcement on panel logins, and network-level access controls are non-negotiable baseline controls.
Detection with SAST
This vulnerability class maps to CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes and CWE-502: Deserialization of Untrusted Data (in the broader sense of structured data reconstruction). SAST detection focuses on identifying patterns where:
- User-controlled input reaches a JSON/array decode operation.
- The decoded result is passed to an object factory (
Yii::createObject(),new $className($config), or equivalent) without an intervening sanitization call. - A sanitization function is applied to a parent structure but not recursively to string-valued children that are subsequently decoded.
Offensive360’s SAST engine flags taint flows from HTTP request parameters through Json::decode() or json_decode() to Yii::createObject() or BaseObject constructor calls, specifically checking for the absence of cleanseConfig() or equivalent stripping of as - and on -prefixed keys on the decoded output. Rules in this category are tagged under our PHP Object Injection / Config Injection ruleset and generate HIGH-confidence findings when the taint path crosses a serialization boundary without re-sanitization.
Dynamic analysis (DAST) can detect this by fuzzing the condition[config] parameter with JSON payloads containing behavior injection keys and monitoring for indicators of successful class instantiation — timing differences, error messages exposing class names, or out-of-band HTTP callbacks from injected code.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-72778-class vulnerabilities and thousands of other patterns — across 60+ languages.