Skip to main content
← All articles
craft cms

Craft patched three RCE paths in October, then disclosed all of them on the same day

Craft patched three RCE paths in October, then disclosed all of them on the same day

On November 2, Craft CMS published six new GitHub Security Advisories in a single batch — all for vulnerabilities that had already been silently fixed in the 5.11.4 and 5.11.5 releases back in early October. That gap between patch and disclosure is itself worth noting, but two of the six advisories are worth reading closely on technical merit alone, because they're the same underlying mistake exploited two different ways: a guard meant to stop Craft from instantiating attacker-chosen classes, defeated twice.

The isset() blind spot

GHSA-hq78-cm2m-h24h (CWE-697, Incorrect Comparison, High severity, reported by 0xkalawy) affects Craft 5.0.0 up to before 5.11.4, patched in 5.11.4 on October 1. The mechanism is almost elegant in how small it is: both craft\helpers\ImageTransforms::normalizeTransform() and Craft::createObject() guard against attacker-controlled class instantiation by checking isset($config['class']) before trusting the value. But isset() returns false for a key whose value is explicitly null — so supplying 'class' => null alongside a sibling __class key slips past both checks. Yii's object configuration gives __class precedence when it's present, so the attacker's class gets instantiated with attacker-controlled properties regardless of what the null value under class was supposed to block.

The advisory names a concrete, reachable path through Craft Commerce: the signed redirect parameter on payments/pay gets rendered as a sandboxed Order template, and a public user with a profile photo can reach the vulnerable call through something as ordinary-looking as {{ customer.photo.getUrl(...) }}. No admin access required — just a storefront with customer photo uploads enabled.

The blocklist that forgot two classes

The second disclosure, GHSA-6m9q-c5q4-3732 (CWE-94, Code Injection, Low severity, reported separately), is lower severity but tells the more interesting story about how incomplete a fix can be while still being a fix. An earlier advisory, GHSA-j5wg-m2pr-35qc, had already identified that create() calls reachable through entry-title-format templates — rendered via Craft's non-sandboxed renderObjectTemplate() — could be abused to instantiate dangerous classes, and the response was to blocklist DOMDocument specifically. GHSA-6m9q-c5q4-3732 shows that blocklist never covered ZipArchive or XMLWriter, both of which can also write arbitrary files to arbitrary paths. If that write lands inside the docroot, it's remote code execution, same as the class it was supposedly blocked for. This one requires the ability to author an entry type's title format — a narrower bar than the Commerce path above, which is presumably why it's rated Low rather than High — but the shape of the bug is identical: trust a specific list of forbidden things instead of an allowlist of permitted ones, and the list will eventually be wrong.

Why a blocklist keeps losing to this exact pattern

Both bugs patched in the same release (5.11.4, October 1) and both disclosed on the same day (November 2) are instances of a single lesson that's easy to state and apparently hard to fully apply: a guard that checks for the presence of a forbidden value is only as good as its model of every way that value can arrive, and a blocklist of forbidden classes is only as good as its model of every dangerous class that exists. isset() has a well-documented blind spot for explicit null; DOMDocument was never the only XML/archive class capable of writing files. Neither gap is exotic once you know to look for it — which is exactly what makes "we already fixed this class of bug" worth re-litigating every time a related code path gets touched, rather than treating a shipped patch as the end of the story.

BM
Blue Moose
The moose behind Blue Moose. Full-stack PHP developer — Drupal by day, Symfony by night, tests always.