Skip to content

Remote Code Execution by non-admin authenticated user

High
angrybrad published GHSA-9wcj-wqqh-cqvg Aug 25, 2026

Package

composer craftcms/cms (Composer)

Affected versions

>= 5.0.0-RC1, < 5.10.12

Patched versions

5.10.12

Description

Craft’s fix for CVE-2025-32432 closed one way into a dangerous function but left the function itself untouched, and it is reachable through another route. On the current release, a logged-in content editor with no admin rights who has access to a Volume can run arbitrary commands on the server.

The fix added a type check to a single controller action (actionGenerateTransform). The function it protects, ImageTransforms::normalizeTransform(), is called from roughly 8 places and hands attacker-shaped data straight to Craft’s object factory. One of the other callers is the element-index endpoint behind the control panel’s asset browser. It takes a request-supplied query and never checks it, so an editor who can browse an asset library reaches the sink and instantiates any class they name.

Description

The fix for CVE-2025-32423 sits at the call site rather than the sink. It rejects a non-string handle before calling the normalizer:

if (!is_string($handle)) {
    throw new BadRequestHttpException('Invalid transform handle.');
}
$transform = ImageTransforms::normalizeTransform($handle);

The normalizer never changed. Given an array, it spreads it into Craft’s object factory:

// src/helpers/ImageTransforms.php:314
return Craft::createObject(['class' => ImageTransform::class, ...$transform]);

A later key in a PHP spread wins, so an attacker who controls $transform can set class and replace the intended ImageTransform::class. Craft’s createObject only rejects the case where both __class and class are present, so a lone class passes. None of this depends on the yii __class bug from CVE-2024-58136, which was fixed in the bundled yii 2.0.55. That is why the gap is still open on the current release rather than something a dependency bump already closed.

Reaching the sink without the type check is a matter of finding another caller. The element-index controller is one. It applies a request-supplied criteria array to the element query:

// src/controllers/ElementIndexesController.php:766
Craft::configure($query, Component::cleanseConfig(ElementHelper::cleanseQueryCriteria($criteria)));

cleanseQueryCriteria() drops a blocklist of keys but keeps withTransforms, and cleanseConfig() strips as and on behavior keys but keeps class. So criteria[withTransforms][0][class] survives into the query, and when the results are populated, the eager loader passes it to normalizeTransform(). The eager-loader runs only when the query returns at least one asset, which is why the attacker needs view access to a volume that contains one.

The terminal step uses a gadget that ships with the product. yii\rbac\PhpManager calls require() on its configured itemFile during init. Point that at Craft’s own request log, and the object injection becomes code execution. Craft writes the full request context, including the User-Agent, to storage/logs/web-<date>.log for any warning or error entry, regardless of devMode. The attacker sends a single request that triggers an error (a POST without a CSRF token returns 400), carrying a PHP payload in the User-Agent, then fires the trigger with itemFile set to that log. The payload runs. It has to avoid quotes and backslashes, which the log escapes and which would break parsing; backticks (`id > ./pwned.txt`) sidestep that.

The admin/ path prefix is required, source has to name a viewable volume or the query returns nothing, viewState[static]=1 avoids an exception, and criteria[limit] has to be set or the index short-circuits before running the query. The lowest privilege that works is a non-admin account with accessCp and viewAssets on a volume that contains an asset, a routine editor role.

Impact

A content editor, or anyone who obtains an editor’s session (accessCp + viewAssets:{volumeId}), gets remote code execution as the web process user. This runs against a production configuration (devMode=false, allowAdminChanges=false, CRAFT_ENVIRONMENT=production).

Report ID: 1577

Severity

High

CVE ID

No known CVE

Weaknesses

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.