This is a loaded nail gun locked inside the maintenance closet
CVE-2026-28783 is a Twig blocklist-bypass in Craft CMS that can turn privileged template access into PHP function execution. The affected lines are Craft 4 from 4.0.0-RC1 up to before 4.17.0, and Craft 5 from 5.0.0-RC1 up to before 5.9.0; the advisory text also references 4.17.0-beta.1 and 5.9.0-beta.1 during disclosure. The bug sits in Craft’s handling of non-Closure Twig arrow functions, where the denylist missed dangerous PHP-call paths.
The vendor/NVD-style 9.1 CRITICAL score overstates enterprise urgency because the decisive prerequisite is not network reachability, it is *already having high-value application privileges* or a compromised admin. In real fleets, PR:H here means post-initial-access, post-credential-theft, or a bad production practice like allowAdminChanges=true; that narrows the reachable population hard enough to downgrade this from internet-burner to authenticated-abuse.
4 steps from start to impact.
Find a reachable Craft control plane
X-Powered-By: Craft CMS or expose recognizable admin paths.- Target runs Craft CMS 4.x or 5.x in an affected range
- Attacker can reach the login or admin utility surface over the network
- Many Craft sites are public-facing, but the vulnerable action path is usually behind authenticated control-panel functionality
- Some operators suppress product headers, move the control panel to a separate domain, or gate admin access with VPN or IP allowlists
Gain privileged app access first
allowAdminChanges enabled on production, a compromised admin account, or an account with access to the System Messages utility. In practice that means phishing an admin, stealing a session, abusing SSO drift, or starting from an already-privileged insider account.- Compromised admin account, or
- A role with access to the System Messages utility, or
allowAdminChangesenabled in production
- This is the big one:
PR:Hmeans the attacker is already deep into the app trust boundary - MFA, SSO conditional access, and admin network segregation all break this step before the CVE matters
- Well-run Craft deployments explicitly set
allowAdminChanges=falsein production
Inject malicious Twig using a permitted feature
- Privileged user can modify a Twig-rendered field, template-like content, or system message path
- The target still permits the vulnerable non-Closure arrow-function behavior
- Not every privileged role can write to every Twig-rendered surface
- Newer projects may already have
enableTwigSandbox=true, which blocks the dangerous non-Closure arrow-function route - Change logging and approval workflows can expose suspicious template edits
Bypass the blocklist and execute server-side actions
- The crafted Twig reaches a vulnerable interpreter path
- The PHP runtime account has access to useful files, network targets, or execution primitives
- Containerized deployments, read-only filesystems, restricted egress, and non-privileged PHP-FPM users reduce blast radius
- The app server may still be isolated from crown-jewel systems even after code execution
allowAdminChanges to false in production — This directly removes one of the vendor-stated prerequisites and shrinks the reachable attack surface. For a MEDIUM verdict there is no noisgate mitigation SLA — go straight to the 365-day remediation window, but this is cheap enough to do in the next normal change window.enableTwigSandbox on existing projects — Craft’s own guidance says existing projects must explicitly enable this to get the stricter Twig behavior. For a MEDIUM verdict there is no mitigation SLA, so deploy during regular configuration management and complete before the remediation deadline.- A generic WAF does not reliably help because the dangerous payload is delivered through authenticated admin functionality and looks like application-authorized content, not commodity edge exploit traffic.
- Unauthenticated perimeter scanning will miss the real risk because the decisive condition is privileged app access, not a banner or open port.
- MFA alone is not enough if the attacker already has a stolen admin session cookie or is operating as a malicious insider with valid privileges.
The supporting signals.
| In-the-wild status | No public active-exploitation confirmation found in the sources reviewed, and CISA KEV does not list CVE-2026-28783 as of 2026-06-02. |
|---|---|
| Proof-of-concept availability | A public exploit write-up/PoC is indexed by CXSecurity and describes authenticated abuse of Twig template access; the vendor PR #18208 also exposes the defensive mechanics. |
| EPSS | 0.00036 (~0.036% 30-day exploitation probability, user-supplied intel). That is very low, and it lines up with the heavy privilege requirement. |
| KEV status | Not KEV-listed. Exact-CVE searches on CISA returned no result for CVE-2026-28783; only older Craft CMS issues appear in KEV. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H says network and low complexity, but the operational truth is the PR:H field: attacker position is already privileged inside the application. |
| Affected versions | GitHub/OSV list Craft CMS >= 4.0.0-RC1, < 4.17.0 and >= 5.0.0-RC1, < 5.9.0 as affected; disclosure pages also referenced 4.17.0-beta.1 and 5.9.0-beta.1 during rollout. |
| Fixed versions | Vendor guidance points to Craft 4.17.0 and 5.9.0 as the resolved releases, with enableTwigSandbox called out as important for existing projects. I found no distro backport advisories in the reviewed sources. |
| Exposure population | Craft is a public-facing CMS, so the product family has real internet exposure. For context, a prior Censys Craft advisory observed 144,333 exposed Craft applications; that is population context, not a count of systems vulnerable to this CVE. |
| Disclosure and reporter | NVD/OSV show publication on 2026-03-04; the GitHub advisory was published 2026-03-02 and credits mHe4am as reporter. |
| Hardening nuance | Craft’s own Securing Craft guidance says new projects have enableTwigSandbox enabled by default, while existing projects need to turn it on explicitly. |
Sources.
Why this verdict
- Downgrade for attacker position:
PR:His not cosmetic here; it means the attacker already owns a privileged app account, an admin session, or a misconfigured production environment. - Downgrade for exposure fraction: plenty of Craft sites are public-facing, but only a subset expose the exact authenticated utility path *and* delegate the needed permissions beyond trusted admins.
- Downgrade for modern controls: MFA, SSO conditional access, admin VPN/IP gating, and EDR on web servers should stop the chain before or immediately after the vulnerable step.
- Keep it above LOW because impact is severe: if the attacker clears the privilege hurdle, the blast radius can jump to server-side code execution, arbitrary file reads, SSRF, and secret theft.
Why not higher?
Because this is not pre-auth and not broadly wormable. A CVE that requires internal authenticated privilege in a CMS control plane is inherently narrower than the classic public-edge RCEs that actually melt patch queues.
Why not lower?
Because the post-condition is still dangerous: this can become full web-tier compromise, not just a mild information leak. Also, Craft is often internet-facing, so the set of organizations with reachable admin surfaces is non-trivial even if the exploit path itself is privileged.
Crowdsourced verification payload.
Run this on the Craft application host from the project root, or pass the root as an argument: ./check-cve-2026-28783.sh /var/www/craft-app. It needs local read access to composer.lock, vendor/, and config/; no root is required unless file permissions are locked down.
#!/usr/bin/env bash
# check-cve-2026-28783.sh
# Detect likely exposure to CVE-2026-28783 in Craft CMS.
# Outputs one of: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN, 3=usage/runtime error
set -u
APP_ROOT="${1:-.}"
if ! command -v php >/dev/null 2>&1; then
echo "UNKNOWN - php not found; cannot evaluate Craft version"
exit 2
fi
if [ ! -d "$APP_ROOT" ]; then
echo "UNKNOWN - app root not found: $APP_ROOT"
exit 3
fi
get_version_from_composer_lock() {
local lock="$1/composer.lock"
[ -f "$lock" ] || return 1
php -r '
$f = $argv[1];
$j = json_decode(file_get_contents($f), true);
if (!$j) exit(1);
$pkgs = array_merge($j["packages"] ?? [], $j["packages-dev"] ?? []);
foreach ($pkgs as $p) {
if (($p["name"] ?? "") === "craftcms/cms") {
$v = $p["version"] ?? "";
$v = ltrim($v, "vV");
echo $v;
exit(0);
}
}
exit(1);
' "$lock"
}
get_version_from_vendor() {
local cj="$1/vendor/craftcms/cms/composer.json"
[ -f "$cj" ] || return 1
php -r '
$f = $argv[1];
$j = json_decode(file_get_contents($f), true);
if (!$j) exit(1);
$v = $j["version"] ?? "";
if (!$v) exit(1);
echo ltrim($v, "vV");
' "$cj"
}
get_version_from_cli() {
if [ -f "$1/craft" ]; then
(cd "$1" && php craft --version 2>/dev/null) | awk '{print $NF}' | sed 's/^v//'
return 0
fi
return 1
}
version_compare_php() {
# args: v1 op v2
php -r 'exit(version_compare($argv[1], $argv[3], $argv[2]) ? 0 : 1);' "$1" "$2" "$3"
}
is_affected() {
local v="$1"
if version_compare_php "$v" ">=" "4.0.0-RC1" && version_compare_php "$v" "<" "4.17.0"; then
return 0
fi
if version_compare_php "$v" ">=" "5.0.0-RC1" && version_compare_php "$v" "<" "5.9.0"; then
return 0
fi
return 1
}
is_in_fixed_major() {
local v="$1"
if version_compare_php "$v" ">=" "4.17.0" && version_compare_php "$v" "<" "5.0.0"; then
return 0
fi
if version_compare_php "$v" ">=" "5.9.0"; then
return 0
fi
return 1
}
sandbox_enabled() {
# Best-effort check only. Existing projects may need this explicitly enabled.
local root="$1"
local files=""
[ -f "$root/config/general.php" ] && files="$files $root/config/general.php"
if [ -d "$root/config" ]; then
# shellcheck disable=SC2046
files="$files $(find "$root/config" -maxdepth 2 -type f \( -name '*.php' -o -name '*.php.dist' \) 2>/dev/null)"
fi
[ -n "$files" ] || return 1
grep -Eqs "enableTwigSandbox[[:space:]]*=>[[:space:]]*true|['\"]enableTwigSandbox['\"][[:space:]]*=>[[:space:]]*true" $files
}
VERSION=""
VERSION="$(get_version_from_composer_lock "$APP_ROOT" 2>/dev/null)" || true
if [ -z "$VERSION" ]; then
VERSION="$(get_version_from_vendor "$APP_ROOT" 2>/dev/null)" || true
fi
if [ -z "$VERSION" ]; then
VERSION="$(get_version_from_cli "$APP_ROOT" 2>/dev/null)" || true
fi
if [ -z "$VERSION" ]; then
echo "UNKNOWN - unable to determine Craft CMS version from composer.lock, vendor metadata, or CLI"
exit 2
fi
if is_affected "$VERSION"; then
echo "VULNERABLE - Craft CMS version $VERSION is in the affected range for CVE-2026-28783"
exit 1
fi
if is_in_fixed_major "$VERSION"; then
if sandbox_enabled "$APP_ROOT"; then
echo "PATCHED - Craft CMS version $VERSION is fixed and enableTwigSandbox appears enabled"
exit 0
else
echo "UNKNOWN - Craft CMS version $VERSION is fixed, but enableTwigSandbox was not confirmed from local config"
exit 2
fi
fi
echo "PATCHED - Craft CMS version $VERSION is outside the affected major/version ranges for CVE-2026-28783"
exit 0