This is not a break-in at the front door, it is an insider with the master key finding an unlocked server room
CVE-2026-28697 is an authenticated admin-side SSTI-to-RCE flaw in Craft CMS. A user with an administrator account can inject Twig into system message or email template fields, call craft.app.fs.write() or related paths, drop a PHP webshell into a web-accessible filesystem or volume, and then execute OS commands through the browser. The affected ranges documented by GitHub/NVD are >= 4.0.0-RC1, < 4.17.0-beta.1 and >= 5.0.0-RC1, < 5.9.0-beta.1.
The vendor/NVD CRITICAL label reflects raw impact after exploitation, not the real attack chain. In practice this is post-auth, admin-only, frequently gated by allowAdminChanges, and further narrowed by needing a writable web-accessible filesystem or volume plus a reachable template-render path. That makes it a serious containment problem after control-plane compromise, but not an internet-wormable or broad pre-auth emergency.
4 steps from start to impact.
Gain Craft admin control
- Authenticated administrator account, or delegated access to the System Messages utility
/adminreachable from the attacker position- MFA/SSO protections bypassed, absent, or already satisfied
- Modern enterprises often front admin panels with SSO, MFA, VPN, or IP allowlists
- A compromised CMS admin is already a substantial security event; many orgs would catch it before follow-on exploitation
- Craft production guidance explicitly recommends disabling
allowAdminChanges
Inject Twig payload into system messages
craft.app.fs.getFilesystemByHandle(...).write(...) or volume-backed writes to place attacker-controlled PHP into storage exposed by the web server. The GHSA includes working payload examples from reporter mHe4am.- Access to Utilities → System Messages or equivalent template-edit path
allowAdminChangesenabled, or utility permissions delegated- A valid filesystem or volume handle known to the attacker
- Many production sites disable
allowAdminChanges=false, removing the easiest path - Not every deployment exposes a writable filesystem or volume back to the web root
- Attackers need enough product knowledge to pick a handle that resolves to a reachable path
.php files. Commodity network scanners generally miss this because the injection lives behind the authenticated UI.Trigger template rendering
- A template-render event must occur after the payload is stored
- Mailer test path or another system-message send path is available
- The application user can write to the target filesystem or volume
- Some environments restrict outgoing mail features or do not exercise the test path routinely
- Application write permissions may be limited to non-executable locations
- Segregated storage and read-only web roots can break the chain here
Call the dropped webshell for OS command execution
curl or a browser and executes commands as the web server user. From there, typical follow-on actions are credential theft from .env, database access, persistence, and lateral movement. The GHSA also documents direct secret-disclosure angles via exposed craft.app properties.- Dropped file ends up in a URL-reachable location
- The web server executes PHP in that location
- Outbound or inbound controls do not block the follow-on command channel
- Uploads often live on object storage, CDN-backed volumes, or non-PHP-served paths
- Many hardened web stacks forbid PHP execution in upload directories
- Even successful RCE usually starts as the low-privilege web user, not immediate root
.php?c= access, process execution from php-fpm/Apache/Nginx workers, EDR child-process alerts, and secret access anomalies.The supporting signals.
| In-the-wild status | No public in-the-wild exploitation evidence found in authoritative sources reviewed, and not listed in CISA KEV as of 2026-05-29. |
|---|---|
| PoC availability | Public PoC is effectively available from the GitHub advisory itself, including payloads that write shell.php; reporter credited as mHe4am. |
| EPSS | User-supplied EPSS is 0.00208, which is very low and aligns with the strong prereq chain. FIRST describes EPSS as a 30-day exploitation probability model via its API/docs. |
| KEV status | No. CISA's Known Exploited Vulnerabilities Catalog does not show this CVE in the reviewed data. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H is mathematically high because post-auth RCE is devastating, but PR:H is the whole story operationally: this starts from admin-level compromise, not initial access. |
| Affected versions | GitHub/NVD list >= 5.0.0-RC1, < 5.9.0-beta.1 and >= 4.0.0-RC1, < 4.17.0-beta.1. |
| Fixed versions | Patched in 5.9.0-beta.1 and 4.17.0-beta.1. The patch series adds tighter Twig sandbox controls and AllowedInSandbox handling in the Craft codebase. |
| Config prerequisite | The published attack prerequisites require an authenticated admin with allowAdminChanges enabled, or access to the System Messages utility. Craft docs recommend setting allowAdminChanges=false in production deployments. |
| Exposure / scanning signal | No authoritative public GreyNoise/Censys/Shodan measurement specific to this CVE was found in reviewed primary sources. Inference: real exposure tracks externally reachable Craft admin panels, not every public Craft site. |
| Disclosure / reporting | Published 2026-03-04 in NVD, with the GitHub advisory published 2026-03-02 and reporter attribution to mHe4am. |
noisgate verdict.
The decisive factor is attacker position: exploitation begins only after an attacker already holds Craft administrative access or equivalent utility access. That turns this from an initial-compromise emergency into a high-impact post-auth escalation path on a relatively narrow population of hardened CMS hosts.
Why this verdict
- Start from 9.1, then subtract for
PR:H: this requires authenticated administrator access or equivalent utility permissions, which implies the attacker has already crossed your identity and control-plane defenses. - Subtract again for population narrowing: only Craft CMS versions in the 4.x/5.x affected bands matter, and not every enterprise even runs Craft at meaningful scale.
- Subtract for production hardening friction: Craft's own docs recommend
allowAdminChanges=falsein production, which removes the easiest attack path in mature deployments. - Subtract for chain complexity after auth: successful weaponization still needs a writable web-accessible filesystem or volume plus a render trigger, so raw impact is broader than real reachability.
- Add back some severity for host compromise impact: if the chain lands, the attacker can execute commands, expose
.envsecrets, and pivot from the web tier.
Why not higher?
This is not unauthenticated remote code execution, and it is not a broad one-click admin browser bug. The chain assumes prior compromise of an administrator or delegated privileged user, and many production deployments break the path with allowAdminChanges=false, non-executable upload paths, or restricted admin exposure.
Why not lower?
Once the prerequisites are met, the outcome is real server-side code execution and secret disclosure, not a cosmetic admin-panel issue. For organizations that do expose Craft admin interfaces or run looser production configs, this can become the cleanest path from CMS-admin compromise to web-tier takeover.
What to do — in priority order.
- Disable admin changes in production — Set
allowAdminChanges=falseanywhere it is operationally viable. This directly breaks the most straightforward attack prerequisite, and because this is a MEDIUM reassessment there is no mitigation SLA — implement during your next hardening cycle rather than treating it as emergency change work. - Put
/adminbehind stronger access controls — Require SSO, MFA, VPN or IP allowlisting for the Craft control panel so the attacker cannot satisfy thePR:Hprerequisite cheaply. There is no mitigation SLA — use your normal identity-hardening window, but prioritize any internet-exposed admin surface. - Block PHP execution from writable paths — Ensure upload directories, volumes, and other app-writable locations cannot execute PHP or other server-side code. That severs the PoC's
write shell -> browse to shellchain even if a template payload lands; for a MEDIUM finding there is no mitigation SLA, so roll this into your standard web-stack hardening work. - Audit admin role and utility permissions — Review who can reach System Messages, Email settings, and other template-edit surfaces; remove dormant admins and enforce least privilege. There is no mitigation SLA — apply in your normal access-review cadence, but do not leave broad shared admin accounts in place.
- Monitor for new executable files in served storage — Use FIM/EDR to alert on
.phpor unexpected executable content created under web-served storage and uploads. There is no mitigation SLA for this severity bucket, but this control has outsized value because it catches both exploitation and other CMS abuse paths.
- A WAF alone does not solve this because the exploit lives behind authenticated admin workflows and uses legitimate application features after login.
- Email security controls do not help; the render trigger is local application behavior, not a malicious inbound message.
- Plugin-only patching misses the point; this is in the core
craftcms/cmspackage and tied to Twig sandbox exposure. - Relying on low EPSS is not a compensating control; it only says broad exploitation is unlikely, not that a compromised admin cannot weaponize it immediately.
Crowdsourced verification payload.
Run this on the Craft application host in the application root that contains composer.lock, composer.json, or vendor/craftcms/cms. Invoke it as bash verify-cve-2026-28697.sh /var/www/craft (replace the path with your install root). No root is required; read access to the app directory is enough.
#!/usr/bin/env bash
# verify-cve-2026-28697.sh
# Checks whether a local Craft CMS installation is in the vulnerable version range for CVE-2026-28697.
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
APP_ROOT="${1:-.}"
if [[ ! -d "$APP_ROOT" ]]; then
echo "UNKNOWN - target path does not exist: $APP_ROOT"
exit 2
fi
find_version() {
local root="$1"
local v=""
# 1) composer.lock
if [[ -f "$root/composer.lock" ]]; then
v=$(php -r '
$f=$argv[1];
$j=json_decode(file_get_contents($f), true);
if (!is_array($j)) exit(1);
foreach (["packages","packages-dev"] as $bucket) {
if (!empty($j[$bucket]) && is_array($j[$bucket])) {
foreach ($j[$bucket] as $pkg) {
if (($pkg["name"] ?? "") === "craftcms/cms") {
$ver = $pkg["version"] ?? "";
$ver = preg_replace("/^v/", "", $ver);
echo $ver;
exit(0);
}
}
}
}
exit(1);
' "$root/composer.lock" 2>/dev/null || true)
if [[ -n "$v" ]]; then
echo "$v"
return 0
fi
fi
# 2) vendor package composer.json
if [[ -f "$root/vendor/craftcms/cms/composer.json" ]]; then
v=$(php -r '
$f=$argv[1];
$j=json_decode(file_get_contents($f), true);
$ver = $j["version"] ?? "";
$ver = preg_replace("/^v/", "", $ver);
echo $ver;
' "$root/vendor/craftcms/cms/composer.json" 2>/dev/null || true)
if [[ -n "$v" ]]; then
echo "$v"
return 0
fi
fi
# 3) composer show fallback
if command -v composer >/dev/null 2>&1 && [[ -f "$root/composer.json" ]]; then
v=$(cd "$root" && composer show craftcms/cms --format=json 2>/dev/null | php -r '
$j=json_decode(stream_get_contents(STDIN), true);
$ver = $j["versions"][0] ?? ($j["version"] ?? "");
$ver = preg_replace("/^v/", "", $ver);
echo $ver;
' 2>/dev/null || true)
if [[ -n "$v" ]]; then
echo "$v"
return 0
fi
fi
return 1
}
VERSION="$(find_version "$APP_ROOT" || true)"
if [[ -z "$VERSION" ]]; then
echo "UNKNOWN - could not determine installed craftcms/cms version"
exit 2
fi
RESULT=$(php -r '
$v = preg_replace("/^v/", "", $argv[1]);
$isVuln = false;
# Affected ranges per GHSA/NVD:
# >= 4.0.0-RC1, < 4.17.0-beta.1
# >= 5.0.0-RC1, < 5.9.0-beta.1
if (version_compare($v, "4.0.0-RC1", ">=") && version_compare($v, "4.17.0-beta.1", "<")) {
$isVuln = true;
}
if (version_compare($v, "5.0.0-RC1", ">=") && version_compare($v, "5.9.0-beta.1", "<")) {
$isVuln = true;
}
echo $isVuln ? "VULNERABLE" : "PATCHED";
' "$VERSION" 2>/dev/null || true)
if [[ "$RESULT" == "VULNERABLE" ]]; then
echo "VULNERABLE - craftcms/cms $VERSION is in the affected range for CVE-2026-28697"
exit 1
elif [[ "$RESULT" == "PATCHED" ]]; then
echo "PATCHED - craftcms/cms $VERSION is outside the affected range for CVE-2026-28697"
exit 0
else
echo "UNKNOWN - version comparison failed for detected version: $VERSION"
exit 2
fi
If you remember one thing.
craftcms/cms instance, confirm whether any production sites still allow admin changes, and identify which admin panels are internet-reachable. For this MEDIUM reassessment there is no noisgate mitigation SLA — go straight to the 365-day remediation window; schedule upgrades to fixed versions within the noisgate remediation SLA of ≤365 days, and fold hardening actions like allowAdminChanges=false, stronger /admin access controls, and non-executable upload paths into your next normal change cycle rather than treating this as an all-hands emergency.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.