This is a loaded nail gun left behind the manager’s desk, not a landmine in the parking lot
CVE-2026-28695 is a Craft CMS server-side template injection path to code execution. In affected craftcms/cms versions >=4.0.0-RC1, <4.17.0-beta.1 and >=5.8.7, <5.9.0-beta.1, the Twig create() function can instantiate attacker-chosen PHP classes; when paired with Symfony\Component\Process\Process, that becomes command execution on the server. The advisory explicitly calls out post-auth exploitation through the control panel, such as an Entry Type Title Format field, and says exploitation requires administrator permissions or access to the System Messages utility.
The vendor's HIGH label captures the technical impact, but it overshoots enterprise patch priority a bit because the decisive gate is privileged authenticated access. This is not an unauthenticated internet spray flaw, not a wormable edge bug, and not KEV-listed; it is a reliable post-auth RCE that matters most where Craft admin access is broad, shared, or externally reachable.
4 steps from start to impact.
Get privileged Craft control-panel access
System Messages utility, per the GHSA. In practice that means stolen admin credentials, SSO session theft, insider misuse, or chaining from another Craft authz bug. Weaponized tooling here is usually just a browser or Burp Suite, because the attack rides normal control-panel workflows.- Internet or network reachability to the Craft admin/control-panel interface
- Valid authenticated account
- Administrator role or
System Messagesutility access
- This is post-initial-access by definition
- SSO, MFA, conditional access, and IP restrictions often stop this before the vuln matters
- Many enterprises tightly limit who can reach or use the Craft control panel
Reach a Twig-backed writable field
Burp, or scripted authenticated HTTP calls.allowAdminChangesenabled in production for the Entry Types path, or access toSystem Messages- Craft Pro if abusing customizable system messages
- Ability to edit the relevant object or message template
- Craft explicitly recommends
allowAdminChanges=falsein production, which removes the easiest settings-based path - Not every deployment exposes these utilities to many users
- Some organizations keep admin changes confined to non-production workflows
Instantiate Symfony Process through Twig create()
create() Twig function to build a Symfony\Component\Process\Process object with attacker-controlled constructor arguments, then triggers execution. The GitHub advisory provides a working proof-of-concept payload, so exploitation is not theoretical. This is effectively a built-in gadget chain rather than memory corruption: the app is doing exactly what the attacker asked it to do.- Affected vulnerable version
- Twig payload accepted and stored
- Bundled
symfony/processdependency present as expected by Craft
- Patched versions route
create()through a restriction that only permitsyii\base\BaseObjectsubclasses - Authenticated admin-only exploitation makes mass scanning and one-shot exploitation less likely
Trigger render and execute on the host
root, which meaningfully increases blast radius. Follow-on tooling becomes standard post-exploitation tradecraft: shells, curl/wget, credential theft, lateral movement, and persistence.- A render event occurs for the tainted template
- The app host allows child process execution
- OS-level controls do not block the spawned command
- Container hardening, read-only filesystems, seccomp, or no-new-privileges can reduce follow-on impact
- EDR can catch unusual child processes from
php-fpm,apache2, ornginxworker contexts - The code runs as the web app account unless the environment is misconfigured
The supporting signals.
| In-the-wild status | No confirmed active exploitation found in the sources reviewed, and not listed in CISA KEV as of 2026-05-29. That sharply separates it from Craft's earlier edge-exploited bugs. |
|---|---|
| Proof-of-concept availability | Public PoC exists in the GitHub advisory itself, using Twig create() plus Symfony\\Component\\Process\\Process. This is low-friction for anyone who already has the required role. |
| EPSS | User-supplied EPSS is 0.00027. That is extremely low and fits the real-world gating factor: privileged authenticated access. FIRST documents score and percentile availability via its API, but percentile was not confirmed in the collected source set. |
| KEV status | No — not present in the CISA Known Exploited Vulnerabilities Catalog during this reassessment. |
| CVSS vector reality check | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H is honest about the big caveat: PR:H. Yes, impact is full-host-class RCE; no, it is not an internet-scale unauthenticated exploit. |
| Affected versions | Authoritative package ranges are >=4.0.0-RC1, <4.17.0-beta.1 and >=5.8.7, <5.9.0-beta.1. The 5.x range starts at 5.8.7 because this flaw is described as a bypass of the earlier fix for CVE-2025-57811. |
| Fixed versions | Fixed upstream in 4.17.0-beta.1 and 5.9.0-beta.1. This is a Composer-delivered app dependency case; no distro backport source was identified in the collected material. |
| Exposure / scanning reality | This CVE is hard to verify from the outside because exploitation needs auth and role context. Product exposure still matters: Censys reported 144,333 exposed Craft CMS applications in a February 2025 Craft advisory, and runZero provides inventory queries for locating Craft CMS internally—useful for scoping, but neither proves this specific flaw is reachable. |
| Disclosure | Published by NVD on 2026-03-04; GitHub reviewed/published the advisory on 2026-03-03 / 2026-03-04 depending on source view. |
| Reporter / source | Primary CNA/source is GitHub, Inc. via the Craft CMS advisory. No separate external researcher credit was visible in the reviewed advisory data. |
noisgate verdict.
The single biggest severity reducer is the attacker position requirement: this bug only matters after an attacker already has a privileged Craft account or equivalent utility access. That makes it a dangerous post-auth blast-radius amplifier, not a primary intrusion path on the open internet.
Why this verdict
- Downgraded for PR:H: the attacker must already hold administrator rights or
System Messagesutility access, which implies prior compromise, insider misuse, or another auth chain. - Further downgraded for reachability friction: the named settings path depends on
allowAdminChangesin production, and Craft explicitly recommends disabling that in prod. - Not downgraded below MEDIUM because impact is real RCE: once the preconditions are met, exploitation is straightforward, public-PoC-backed, and can end in full host compromise.
Why not higher?
This is not an unauthenticated edge bug, not KEV-listed, and not a low-privilege user-to-root scenario. Every serious attack chain starts with the attacker already crossing your authentication and authorization boundary, which is a major downward pressure on urgency.
Why not lower?
If the attacker has the required role, this is not a nuisance bug—it is a clean path from CMS privilege to server command execution. That jump from application administration to host-level execution materially increases blast radius, especially on weakly isolated containers or shared web infrastructure.
What to do — in priority order.
- Disable admin changes in production — Set
allowAdminChanges=falseon production Craft instances to remove the easiest control-panel path called out in the advisory. For a MEDIUM verdict there is no noisgate mitigation SLA, so treat this as hardening you should roll in with normal config governance rather than emergency change windows. - Restrict control-panel access — Put the Craft admin path behind SSO + MFA, IP allowlisting, VPN, or a reverse-proxy access policy so fewer identities can ever satisfy the vuln's first prerequisite. This directly attacks the most important risk factor: privileged authenticated reachability.
- Tighten utility permissions — Review who can access
System Messagesand similar high-trust control-panel features, and remove that access from broad admin or content teams. The GHSA explicitly treats that utility as an alternate exploitation route. - Alert on PHP child processes — Create EDR/SIEM detections for
php-fpm,apache2, ornginxspawning shells or process-execution children. This helps catch successful exploit attempts and follow-on activity even if the malicious payload is tucked inside legitimate CMS content. - Inventory Craft by package version — Use SBOM, Composer lockfile parsing, or app inventory to identify
craftcms/cmsin the affected ranges and queue upgrades inside the normal remediation program. This flaw is much easier to manage through software inventory than through perimeter scanning.
- A WAF alone will not reliably save you here; the exploit is authenticated, application-specific, and lives inside normal control-panel fields.
- Hiding the admin URL is not a control. If an attacker already has a session or valid credentials, obscurity does nothing.
- Network-only vulnerability scanning is insufficient because scanner reach rarely includes the required authenticated role and utility context.
Crowdsourced verification payload.
Run this on the target Craft host from the application root that contains composer.lock or vendor/. Invoke it as bash ./check-cve-2026-28695.sh /var/www/craft-app. It needs only read access to the app files; no root is required.
#!/usr/bin/env bash
# check-cve-2026-28695.sh
# Determine whether a Craft CMS installation is vulnerable to CVE-2026-28695
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
APP_ROOT="${1:-.}"
COMPOSER_LOCK="$APP_ROOT/composer.lock"
COMPOSER_JSON="$APP_ROOT/composer.json"
have_cmd() {
command -v "$1" >/dev/null 2>&1
}
ver_ge() {
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | tail -n1)" = "$1" ]
}
ver_lt() {
[ "$1" != "$2" ] && [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ]
}
extract_version_php() {
local file="$1"
php -r '
$f = $argv[1];
if (!is_file($f)) { exit(2); }
$j = json_decode(file_get_contents($f), true);
if (!is_array($j)) { exit(3); }
$pkgs = [];
if (isset($j["packages"]) && is_array($j["packages"])) $pkgs = array_merge($pkgs, $j["packages"]);
if (isset($j["packages-dev"]) && is_array($j["packages-dev"])) $pkgs = array_merge($pkgs, $j["packages-dev"]);
foreach ($pkgs as $p) {
if (($p["name"] ?? "") === "craftcms/cms") {
echo $p["version"] ?? "";
exit(0);
}
}
exit(4);
' "$file" 2>/dev/null
}
normalize() {
local v="$1"
v="${v#v}"
printf '%s' "$v"
}
if ! have_cmd php; then
echo "UNKNOWN: php CLI not found"
exit 2
fi
VERSION=""
SOURCE=""
if [ -f "$COMPOSER_LOCK" ]; then
VERSION="$(extract_version_php "$COMPOSER_LOCK")"
if [ -n "$VERSION" ]; then
SOURCE="$COMPOSER_LOCK"
fi
fi
if [ -z "$VERSION" ] && [ -f "$COMPOSER_JSON" ]; then
# Fallback: infer declared constraint, not installed version
VERSION="$(php -r '
$f = $argv[1];
$j = json_decode(file_get_contents($f), true);
if (!is_array($j)) exit(1);
$req = $j["require"]["craftcms/cms"] ?? "";
echo is_string($req) ? $req : "";
' "$COMPOSER_JSON" 2>/dev/null)"
if [ -n "$VERSION" ]; then
echo "UNKNOWN: only composer constraint found in $COMPOSER_JSON -> $VERSION"
exit 2
fi
fi
if [ -z "$VERSION" ]; then
echo "UNKNOWN: could not determine installed craftcms/cms version from $APP_ROOT"
exit 2
fi
RAW_VERSION="$VERSION"
VERSION="$(normalize "$VERSION")"
# Exact vulnerable ranges from advisory:
# 4.0.0-RC1 <= v < 4.17.0-beta.1
# 5.8.7 <= v < 5.9.0-beta.1
if ver_ge "$VERSION" "4.0.0-RC1" && ver_lt "$VERSION" "4.17.0-beta.1"; then
echo "VULNERABLE: craftcms/cms $RAW_VERSION detected from $SOURCE (matches 4.x vulnerable range for CVE-2026-28695)"
exit 1
fi
if ver_ge "$VERSION" "5.8.7" && ver_lt "$VERSION" "5.9.0-beta.1"; then
echo "VULNERABLE: craftcms/cms $RAW_VERSION detected from $SOURCE (matches 5.x vulnerable range for CVE-2026-28695)"
exit 1
fi
if ver_ge "$VERSION" "5.9.0-beta.1" || ver_ge "$VERSION" "4.17.0-beta.1"; then
echo "PATCHED: craftcms/cms $RAW_VERSION detected from $SOURCE"
exit 0
fi
echo "PATCHED: craftcms/cms $RAW_VERSION detected from $SOURCE (outside known vulnerable ranges for CVE-2026-28695)"
exit 0
If you remember one thing.
allowAdminChanges is still enabled in production. Because the reassessed verdict is MEDIUM, there is no noisgate mitigation SLA — go straight to the 365-day remediation window; use routine change control to harden admin access and remove risky utility permissions, then complete the actual Craft upgrade within the noisgate remediation SLA of ≤365 days. If a particular site has internet-exposed admin access, shared admin accounts, or weak MFA, move that instance up locally even though the portfolio-wide rating stays MEDIUM.Sources
- NVD CVE record
- GitHub Advisory Database JSON for GHSA-94rc-cqvm-m4pw
- Craft CMS fix commit e31e508
- Craft docs: allowAdminChanges
- Craft docs: System Messages are Twig-evaluated
- Craft supported versions / lifecycle
- CISA Known Exploited Vulnerabilities Catalog
- Censys advisory noting exposed Craft CMS population
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.