This is a loaded nail gun locked in the maintenance closet, not a rifle lying in the lobby
CVE-2026-28784 is a Twig server-side template injection path in Craft CMS that can end in remote code execution on the web server. The vulnerable window spans Craft >= 4.0.0-RC1 and < 4.17.0-beta.1, plus >= 5.0.0-RC1 and < 5.9.0-beta.1; the vendor says production-safe stable fixes landed in 4.16.18 and 5.8.22. The exploit path uses the Twig map filter in text fields that accept Twig input under Settings in the control panel, or through the System Messages utility.
The vendor/NVD 7.2 HIGH score is technically defensible in a lab because successful exploitation is full server compromise. In the field, though, this is heavily gated: it needs authenticated control-panel access plus either administrator rights with allowAdminChanges enabled or a non-admin account with access to the System Messages utility. That is post-initial-access, narrow-population, and often blocked by sane Craft hardening, so the real-world patch priority comes down from HIGH to a solid MEDIUM.
4 steps from start to impact.
Gain privileged Craft control-panel access
- Craft control panel is reachable to the attacker
- Valid authenticated account exists
- Account has admin rights or access to the System Messages utility
- Many Craft admin panels are VPN-restricted, IP-limited, or SSO-protected
- MFA and identity telemetry should catch the account takeover step
- System Messages utility access is uncommon outside select operators
Reach a Twig-accepting admin surface
map filter path referenced in the advisory and associated patch work in craftcms/cms#18208. This is an application-feature abuse step, not generic web fuzzing.- A vulnerable Craft version is installed
- Affected UI path is present and reachable in the current role
allowAdminChangesis enabled for the admin path, or System Messages utility access exists
- Craft explicitly recommends
allowAdminChanges=falsein production - Some enterprises hide or operationally lock Settings changes in production
- Least-privilege RBAC shrinks the set of users who can reach these screens
Trigger Twig SSTI into code execution
- Payload reaches the vulnerable template-evaluation path
- Application executes Twig in the affected mode
- Host-level controls do not block the resulting child process or file abuse
- Modern EDR can catch suspicious child processes spawned by
php-fpm,apache2, ornginxworkers - Containerized or constrained PHP runtimes can reduce post-exec options
- No public mass-exploitation evidence or broadly shared PoC was located
Pivot from CMS host to broader environment
- Secrets are accessible from the Craft runtime
- Network egress or local privilege paths permit follow-on activity
- Secret rotation, network segmentation, and IMDS protections can contain follow-on damage
- Read-only containers or non-root service accounts limit persistence options
The supporting signals.
| In-the-wild status | No public evidence of active exploitation found in the reviewed sources. Not KEV-listed based on the current CISA KEV catalog. |
|---|---|
| Proof-of-concept availability | I did not find a public exploit repo or turnkey PoC tied to CVE-2026-28784 or GHSA-qc86-q28f-ggww. Public technical detail exists in the GitHub advisory and patch PR, which is enough for a capable operator but not the same thing as mass-ready exploit code. |
| EPSS | 0.021% (6th percentile) per the GitHub Advisory Database entry. That is very low and consistent with a high-friction authenticated/admin-only path. |
| KEV status | No as of 2026-06-02. No CVE-specific CISA KEV entry was found; use the catalog root as the authoritative check. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H = network reachable, easy to trigger *once you already hold high privileges*, no user interaction, and full CIA loss on the affected host. The PR:H term is the whole story here. |
| Affected versions | Advisory package range: >= 4.0.0-RC1, < 4.17.0-beta.1 and >= 5.0.0-RC1, < 5.9.0-beta.1 per GHSA and GitLab GLAD. |
| Fixed versions | First unaffected package tags are 4.17.0-beta.1 and 5.9.0-beta.1, while the vendor says stable mitigation updates were shipped in 4.16.18 and 5.8.22 per NVD and the Craft GHSA. No distro backport guidance was found. |
| Exposure reality | Craft itself is internet-exposed at non-trivial scale: a prior Craft-specific Censys advisory observed 144,333 exposed applications. That number is not specific to this CVE, but it shows the platform has meaningful public footprint. The exploitable subset for this CVE is much smaller because the vulnerable path sits behind authenticated admin/utility workflows. |
| Disclosure date | Published 2026-03-04 in NVD; GitHub security advisory published 2026-03-02 and GitHub Advisory Database published 2026-03-03. |
| Researcher / reporter | Credited finder: RajChowdhury240. Reporter: rlarabee. Source: vendor GHSA. |
noisgate verdict.
The decisive factor is attacker position: this bug generally requires authenticated control-panel access plus elevated rights or a niche utility permission, which makes it a post-compromise amplifier, not an initial-access bug. The impact is severe on the individual host, but the reachable population is sharply reduced by PR:H, Craft's recommended allowAdminChanges=false production setting, and the rarity of System Messages access outside trusted operators.
Why this verdict
- Downgrade for attacker position: the chain starts with authenticated control-panel access, and the common branch needs high privileges. That implies prior compromise or insider access, which is a major downward adjustment from the vendor's 7.2 baseline.
- Downgrade for deployment friction: the admin path also expects
allowAdminChanges=true, while Craft explicitly recommendsfalsein production. In mature deployments, that setting alone kills the easiest exploit path. - Keep it at MEDIUM, not LOW: if the attacker *does* have the needed role, the outcome is still server-side code execution with full confidentiality, integrity, and availability impact on the Craft host.
Why not higher?
This is not unauthenticated remote exploitation, and it is not broadly reachable across the exposed Craft population. The exploit chain requires either admin-level control-panel access plus a non-recommended production setting, or a narrower non-admin role with access to System Messages. There is also no KEV listing, no public exploitation evidence found, and EPSS is extremely low.
Why not lower?
Remote code execution is still remote code execution once the attacker clears the gate. If you already have exposed Craft admin surfaces, shared admin credentials, or weak SSO/MFA hygiene, this becomes a practical second-stage kill shot against the application server and its secrets. That keeps it above LOW.
What to do — in priority order.
- Set
allowAdminChangestofalsein production — Do this first because it directly cuts off the main admin-side exploit branch documented by Craft. For a MEDIUM verdict there is no noisgate mitigation SLA; go straight to the remediation window, but this config hardening should still be applied during the same change cycle because it meaningfully reduces exposure. - Restrict control-panel exposure — Put the Craft control panel behind VPN, identity-aware proxy, IP allowlists, or equivalent admin-plane segmentation. This shrinks the pool of attackers who can even attempt the authenticated prerequisite; for a MEDIUM verdict there is no mitigation SLA — go straight to the 365-day remediation window, but this is worthwhile hygiene on any internet-facing CMS.
- Review and trim System Messages utility access — Audit which roles can reach the System Messages utility and remove it from everyone except the handful of operators who actually need it. This matters because the advisory explicitly names that utility as the alternative exploitation path when
allowAdminChangesis disabled. - Watch for PHP child-process anomalies — Tune EDR or host monitoring to alert on suspicious child processes from
php-fpm,apache2, ornginxworker contexts, plus unusual shell, curl, wget, python, or package-manager execution. It will not prevent the bug, but it gives you a fighting chance to catch successful SSTI-to-RCE behavior. - Enforce strong admin authentication — Backstop the biggest real-world prerequisite by enforcing MFA, SSO, impossible-travel alerts, and session revocation for Craft admins. The vulnerability's practical exploitability collapses if attackers cannot cheaply get or reuse privileged panel access.
- A perimeter WAF by itself won't save you; the vulnerable actions occur in authenticated admin workflows and the payload rides valid application semantics.
- Relying on 'the panel isn't linked publicly' is not a control; if the URL is reachable and credentials are phished or reused, the prerequisite is satisfied.
- Generic vulnerability scans often miss this because the path is authenticated, role-gated, and feature-specific.
Crowdsourced verification payload.
Run this on the Craft CMS host or inside the application container, pointing it at the application root that contains composer.lock or vendor/. Example: sudo ./check-cve-2026-28784.sh /var/www/craft or ./check-cve-2026-28784.sh /srv/app. It needs read access to the app files and a working php binary for version parsing; root is not required if the app directory is readable.
#!/usr/bin/env bash
# check-cve-2026-28784.sh
# Detect likely exposure to CVE-2026-28784 in Craft CMS.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN, 3=usage/runtime error
set -u
TARGET="${1:-.}"
if ! command -v php >/dev/null 2>&1; then
echo "UNKNOWN - php binary not found; cannot parse Craft version reliably"
exit 2
fi
if [ ! -d "$TARGET" ]; then
echo "UNKNOWN - target path does not exist: $TARGET"
exit 3
fi
LOCKFILE="$TARGET/composer.lock"
INSTALLED_JSON="$TARGET/vendor/composer/installed.json"
CRAFT_COMPOSER_JSON="$TARGET/vendor/craftcms/cms/composer.json"
VERSION=""
SOURCE=""
if [ -f "$LOCKFILE" ]; then
VERSION=$(php -r '
$f=$argv[1];
$j=json_decode(file_get_contents($f), true);
if (!is_array($j)) exit(1);
$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(1);
' "$LOCKFILE" 2>/dev/null)
if [ -n "$VERSION" ]; then SOURCE="$LOCKFILE"; fi
fi
if [ -z "$VERSION" ] && [ -f "$INSTALLED_JSON" ]; then
VERSION=$(php -r '
$f=$argv[1];
$j=json_decode(file_get_contents($f), true);
if (!is_array($j)) exit(1);
$pkgs=[];
if (isset($j["packages"]) && is_array($j["packages"])) {
$pkgs=$j["packages"];
} elseif (array_is_list($j)) {
$pkgs=$j;
} elseif (isset($j["versions"]) && is_array($j["versions"])) {
$pkgs=array_values($j["versions"]);
}
foreach ($pkgs as $p) {
if (($p["name"] ?? "") === "craftcms/cms") {
echo $p["version"] ?? ($p["pretty_version"] ?? "");
exit(0);
}
}
exit(1);
' "$INSTALLED_JSON" 2>/dev/null)
if [ -n "$VERSION" ]; then SOURCE="$INSTALLED_JSON"; fi
fi
if [ -z "$VERSION" ] && [ -f "$CRAFT_COMPOSER_JSON" ]; then
VERSION=$(php -r '
$f=$argv[1];
$j=json_decode(file_get_contents($f), true);
if (!is_array($j)) exit(1);
echo $j["version"] ?? "";
' "$CRAFT_COMPOSER_JSON" 2>/dev/null)
if [ -n "$VERSION" ]; then SOURCE="$CRAFT_COMPOSER_JSON"; fi
fi
if [ -z "$VERSION" ]; then
echo "UNKNOWN - could not determine installed Craft CMS version from composer metadata"
exit 2
fi
RESULT=$(php -r '
$v=trim($argv[1]);
$norm=preg_replace("/^v/i", "", $v);
function vulnerable($ver) {
# Known stable fixed versions from the vendor/NVD narrative:
# 4.x fixed in 4.16.18, 5.x fixed in 5.8.22.
# Advisory package ranges also note first unaffected prereleases:
# 4.17.0-beta.1 and 5.9.0-beta.1.
if (preg_match("/^4\./", $ver)) {
return version_compare($ver, "4.0.0-RC1", ">=") && version_compare($ver, "4.16.18", "<");
}
if (preg_match("/^5\./", $ver)) {
return version_compare($ver, "5.0.0-RC1", ">=") && version_compare($ver, "5.8.22", "<");
}
return null;
}
$isVuln=vulnerable($norm);
if ($isVuln === true) {
echo "VULNERABLE";
exit(1);
}
if ($isVuln === false) {
echo "PATCHED";
exit(0);
}
echo "UNKNOWN";
exit(2);
' "$VERSION")
STATUS=$?
echo "$RESULT - detected craftcms/cms version $VERSION from $SOURCE"
exit $STATUS
If you remember one thing.
allowAdminChanges is still enabled in production. Because this is MEDIUM, there is no noisgate mitigation SLA — go straight to the 365-day remediation window; still, harden the admin plane and disable allowAdminChanges in production during the next normal change cycle, then complete the actual vendor update within the noisgate remediation SLA of 365 days. If any affected Craft admin surface is public and weakly protected, move that subset to the front of the queue rather than treating the entire CVE as emergency patching.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.