Blackfort Technology
Incident AnalysisJuly 22, 2026·Christian Gebhardt

CVE-2026-65008: Critical Remote Code Execution in Grav CMS before 2.0.7

A critical flaw (CVSS 9.3) in the PHP CMS Grav allows code execution through a page directive. We show the mechanism, a real lab execution, the detection rule firing in the SIEM and why upgrading to 2.0.7 is the effective fix.

Follow Blackfort on LinkedIn

Security incidents, technical analyses and hands-on insights — straight to your LinkedIn feed.

Follow now →
Abstract depiction of a remote code execution flaw in a PHP web CMS with a shell injected through a directive

On 21 July 2026 the CVE numbering authority VulnCheck published CVE-2026-65008 — a critical flaw in the PHP CMS Grav in all versions before 2.0.7. It allows arbitrary shell commands to run on the server (CWE-94, code injection). It is rated CVSS v4.0 9.3 and CVSS v3.1 9.8, both critical. The bug is fixed in Grav 2.0.7.

Two-minute analysis: the mechanism of the Grav RCE, how the attack becomes visible in the SIEM and why upgrading to 2.0.7 works.
Critical facts

CVE ID: CVE-2026-65008

GHSA: GHSA-fj2p-qj2f-74v5 (getgrav)

CVSS v4.0: 9.3 (critical)

CVSS v3.1: 9.8 (critical)

CWE: CWE-94 (code injection)

Affected: Grav CMS in all versions before 2.0.7

Fixed: Grav 2.0.7

Published: 21 July 2026 (VulnCheck)

We reproduced the case in an isolated lab using the real Grav releases — from the exploit through the detection firing in the SIEM to the fix. This article explains where the flaw sits, how an attack unfolds, how it becomes visible at the endpoint and which measure stops it; backed by the material from that run.

The problem: a callable string with no allowlist

Grav builds forms and fields from so-called blueprints. A blueprint can produce field values dynamically by calling a function — a directive of the form data-*@. To do so, the method Blueprint::dynamicData() takes a callable string written as Class::method together with its arguments and hands it without any allowlist straight to call_user_func_array(). Nothing checks which function ends up there.

The second building block is a public helper, Grav\Common\Utils::arrayFilterRecursive($source, $fn). It calls the supplied function $fn on every element of an array. Wire it into the dynamic directive as the callable and give it system as the function to call, and it runs a shell command that sits in the array. Through that detour, the missing check in dynamicData() turns into full code execution.

What makes it serious: the Form pluginprocesses a page's frontmatter through exactly this path. The malicious directive only has to sit in a page's frontmatter.

Scenarios: who plants it and who triggers it

The attack has two separate roles, and that is what makes it treacherous.

Planting. An account with the admin.pages or api.pages.writepermission writes the directive into a page's frontmatter. That is an editor or API write permission, not administrative access to the server.

Triggering. The command runs as soon as anyone opens the prepared page — including an unauthenticated visitor. The commands run with the privileges of the web-server user.

In the lab we reproduced exactly this core: real Grav 2.0.4 (the upstream release, installed via composer), invoked through the documented data-*@ directive. The original code ran the command as www-data:

Real lab execution: Grav 2.0.4 runs a shell command as the www-data user through the blueprint directive
Real lab execution: Grav 2.0.4 runs a command as www-data.
Honest about the scope of our reproduction

We triggered the vulnerable code path (dynamicData()call_user_func_array() → the system trampoline) directly through the blueprint interface of the real Grav release — the route a malicious frontmatter directive takes. We did not stand up the preceding HTTP and form submission against a running website; the executed code is the same.

The fix: detect and patch — both shown in the lab

Detection. An attack like this shows at the endpoint: the web-server user suddenly starts a shell — anomalous behaviour for a PHP web server. For that we wrote a dedicated detection rule and connected the Grav host to a Wazuh instance. As soon as the www-data account starts a shell, the rule (ID 100710) fires. After the exploit runs, the alert is visible in the Wazuh dashboard — with the triggering process (/usr/bin/dash), the user (uid 33, www-data) and the mapping to MITRE ATT&CK T1059.004:

Real Wazuh alert in the dashboard: rule 100710 fires because the web-server user www-data starts a shell, mapped to MITRE ATT&CK T1059.004
Real Wazuh alert: the rule fires on “web-server user starts a shell”.

That makes the detection demonstrated: the rule is written, fired in the lab and shown here as a dashboard screenshot. A raw log alone would not stand in for that proof.

Patching. The primary and effective measure remains the upgrade to Grav 2.0.7. The patched version checks before the call whether a dynamic directive is dangerous: a directly dangerous function (system, exec and the like) is refused, and every argument is scanned so a harmless helper cannot serve as a trampoline for a dangerous function. That removes exactly the system trampoline via arrayFilterRecursive. We ran the same payload against real Grav 2.0.7 — the call is refused, no command runs:

Fix confirmed: Grav 2.0.7 refuses the same payload, no shell command is executed
Fix confirmed: Grav 2.0.7 refuses the same payload.

If an upgrade is not immediately possible, the mechanism suggests two useful interim steps: keep the number of accounts with admin.pages and api.pages.write small and make their activity auditable, and switch on the detection shown above (web-server user starts a shell). Neither replaces the patch, but both shorten the window.

On exploitation status

As of this research, CVE-2026-65008 is not listed in the CISA KEV catalog, and active exploitation in the wild is not publicly documented. For a critical, well-documented code execution that is more a question of timing than of likelihood — patch prioritisation should not depend on it.

What it means for operations

A single critical flaw in widely deployed web software is a classic prioritisation case: where is a vulnerable Grav instance running, is it reachable, and how fast does the patch reach it? That is the view a Threat Exposure Filter provides — surface exposed instances and order patching by exploitability. Detecting vulnerable versions in the estate and rolling out the fix in an orderly way is the job of vulnerability and patch management; a vulnerability scan finds the affected systems.

Note

This article explains a technical matter and is not legal advice. It describes publicly available research for detection and protection purposes; it contains no exploitation guidance. The vulnerability was reported by YuvalMil, MatiHub25 and LeonKaya. Sources: CVE-2026-65008, GHSA-fj2p-qj2f-74v5 (getgrav), VulnCheck advisory, NVD. The material shown comes from our own lab reproduction using the real Grav releases 2.0.4 and 2.0.7 and a segment-local Wazuh instance.

Kontakt aufnehmen

IT security for your organisation

Blackfort Technology supports organisations with NIS2 compliance, OT security and the protection of critical infrastructure – from analysis to implementation.