All vulnerabilities
CVE-2026-69088GHSA-7pgq-cr25-xvc8

getgrav/grav: Grav: Incomplete callable validation in blueprint dynamic fields allows arbitrary static method invocation and file disclosure

getgrav getgrav/gravcomposerLibraries & packages
CVSS
8.1High
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 3 Aug 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.29%Chance of exploitation activity in the next 30 days, higher than 22% of CVEs. Scored 26 Oct 2026FIRST EPSS
Fixed in2.0.11Fixed versionGitHub
Published3 Aug 2026Updated 26 Oct 2026CVE List

What fixes it

  • 2.0.11getgrav/grav, composerGitHubcomposer require "getgrav/grav:^2.0.11"
  • 2.0.11gravCVE List
  • 2.0.11getgrav/grav, PackagistOSVcomposer require "getgrav/grav:^2.0.11"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

Grav CMS's blueprint dynamic-field callable guard can be bypassed with a fully-qualified Class::method string, letting an account with only page-editing rights (admin.pages, not super-admin) plant a directive in a page's form-field frontmatter that invokes an arbitrary public static PHP method with attacker-controlled arguments. Using built-in gadget methods this yields, at minimum, arbitrary reading of any server-readable file (disclosed to anonymous visitors of the crafted page) and arbitrary creation/copying of files and directories under the web-server account.

Details

Blueprint::isSafeDynamicCall() (system/src/Grav/Common/Data/Blueprint.php, method around line 488) is meant to block dangerous callables named in a blueprint's dynamic-field directives (data-*@). It only consults its dangerous-name denylist when the callable string does not contain :::

if (is_string($function) && !str_contains($function, '::') && Utils::isDangerousFunction($function)) {
    return false;
}

Any callable string containing :: — i.e. every Class::method static call — skips the check entirely and is passed to call_user_func_array() at Blueprint::dynamicData() (Blueprint.php, around line 461) and FlexDirectory::dynamicDataField() (system/src/Grav/Framework/Flex/FlexDirectory.php, around line 937). There is no allowlist restricting which classes or methods may be invoked this way; only the call's *arguments* are (separately) scanned for smuggled dangerous callables, never the target itself.

Utils::isDangerousFunction() (system/src/Grav/Common/Utils.php) classifies any string containing a colon (str_contains($name, ":")) or a namespace backslash as dangerous — so a qualified Class::method string would be rejected *if* it ever reached this function. The !str_contains($function, '::') condition in isSafeDynamicCall() ensures it never does, which is what leaves qualified static calls entirely unscreened. (Whether the exemption was intended to admit legitimate Class::method option-providers is a plausible reading of the surrounding code, but the intent is not established here.)

This is an incomplete fix of two recently published advisories — one addressing page editors executing hidden callables via form-field settings, the other extending the same guard to Flex directories. The guard those fixes introduced never covered qualified static calls. Grav's own permission model separates page-content code execution into a distinct, higher privilege (admin.pages_twig) from plain page editing (admin.pages), so invoking arbitrary methods from an admin.pages-authored page is a genuine trust-boundary bypass, not editor capability by design.

PoC

Tested on Grav develop at commit db8c1fc (which self-reports version 2.0.11) with the admin and form plugins, using an account granted only admin.login + admin.pages (page editor, not super-admin). The same guard is present in every current release from 2.0.7 through 2.0.10. Base URL shown as https://grav.example.

A. Arbitrary file read (confidentiality)

  1. Log in to /admin as the page-editor account (GET /admin for the login nonce, then POST task=login).
  2. Save a page via the standard admin endpoint, POST /admin/pages/<route> with the session cookie, the admin nonce, and data[frontmatter] containing a form field with a download gadget directive:
    forms:
      x:
        fields:
          y:
            type: text
            data-opts@:
              - 'Grav\Common\Utils::download'
              - '/etc/passwd'
              - false
              - 0
              - 1024
              - mime: 'text/plain'

The server returns HTTP 200 and accepts the save — the data-opts@ directive is not rejected.

  1. As an unauthenticated visitor (no cookies), request the saved page, e.g. GET /<route> (use a fresh query string to avoid a cached copy; immediately after saving, a first request may 404 while the flat-file page index catches up — retry moments later). The response is HTTP 200 with the raw contents of /etc/passwd in the body (root:x:0:0:...). Pointing the path at user/accounts/<name>.yaml instead returns that account file, including its hashed_password: bcrypt line — i.e. an anonymous visitor obtains a stored administrator's password hash.

B. Arbitrary file/directory write (integrity) — verified

Using the same mechanism with Grav\Common\Filesystem\Folder::copy (a public static method taking source and destination paths), a page editor caused the server to copy an existing page directory to an attacker-chosen new path under user/pages/; the newly created page then rendered its (attacker-controlled) content at the new route over plain HTTP. This demonstrates attacker-controlled creation of files/directories anywhere the web-server account can write. Folder::move and Folder::delete are equally reachable (their destructive nature was not exercised).

Impact

A page editor (an admin.pages-only account, not super-admin) can, through a page they author:

  • Read any server-readable file, disclosed to any anonymous, unauthenticated visitor of the crafted page — including user/accounts/*.yaml, which stores account metadata and bcrypt password hashes. An attacker may attempt offline cracking of a disclosed hash; recovery of a weak or reused administrator password could lead to full admin-panel compromise. Other secrets on disk (site/plugin config, environment files) are equally exposed.
  • Create or overwrite files and directories under the web-server account (demonstrated via Folder::copy), with Folder::move/Folder::delete additionally reachable for destructive tampering.

Because the guard permits *any* public static method, the reachable impact is bounded only by the gadget surface of the loaded codebase, not by this report's demonstrated cases.

Text from GitHub Advisory Database

Affected versions 8

  • getgrav/grav, composer>= 2.0.7, <= 2.0.10GitHub
  • gravaffected: 2.0.7 before 2.0.11CVE List
  • gravunaffected: 2.0.11CVE List
  • getgrav/grav, Packagistfrom 2.0.7, fixed in 2.0.11OSV
  • getgrav/grav, Packagist2.0.10OSV
  • getgrav/grav, Packagist2.0.7OSV
  • getgrav/grav, Packagist2.0.8OSV
  • getgrav/grav, Packagist2.0.9OSV

References 6