getgrav/grav: Grav: Incomplete callable validation in blueprint dynamic fields allows arbitrary static method invocation and file disclosure
What fixes it
- 2.0.11getgrav/grav, composerGitHub
composer require "getgrav/grav:^2.0.11" - 2.0.11gravCVE List
- 2.0.11getgrav/grav, PackagistOSV
composer 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)
- Log in to
/adminas the page-editor account (GET/adminfor the login nonce, then POSTtask=login). - Save a page via the standard admin endpoint,
POST /admin/pages/<route>with the session cookie, the admin nonce, anddata[frontmatter]containing a form field with adownloadgadget 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.
- 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 may404while the flat-file page index catches up — retry moments later). The response isHTTP 200with the raw contents of/etc/passwdin the body (root:x:0:0:...). Pointing the path atuser/accounts/<name>.yamlinstead returns that account file, including itshashed_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), withFolder::move/Folder::deleteadditionally 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
- github.com/getgrav/grav/commit/446d7f8f255918c8234634fe1dc2c9c77a0c673eFixvia GitHub
- github.com/getgrav/grav/releases/tag/2.0.11Fixvia GitHub
- vulncheck.com/advisories/grav-cms-through-arbitrary-method-invocation-via-blueprintAdvisoryvia CVE List
- github.com/advisories/GHSA-7pgq-cr25-xvc8Advisoryvia GitHub
- github.com/getgrav/grav/security/advisories/GHSA-7pgq-cr25-xvc8Exploitvia CVE List
- nvd.nist.gov/vuln/detail/CVE-2026-69088Referencevia GitHub