GHSA-7pgq-cr25-xvc8 CVE-2026-69088
Grav: Incomplete callable validation in blueprint dynamic fields allows arbitrary static method invocation and file disclosure
- Severity
- 8.6 / 10
- High · CVSS 4.0
- Exploit chance (EPSS)
- 0.29%
- Chance of exploitation in the next 30 days. Higher than 22.23% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
- CWE-470 Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')
- CWE-94 Improper Control of Generation of Code ('Code Injection')
- Fix
2.0.11- Upgrade getgrav/grav to this version or later
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:
The server returnsforms: x: fields: y: type: text data-opts@: - 'Grav\Common\Utils::download' - '/etc/passwd' - false - 0 - 1024 - mime: 'text/plain'HTTP 200and accepts the save — thedata-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.
Affected versions
Packagist
| Package | Vulnerable | Fixed in |
|---|---|---|
| getgrav/grav | >= 2.0.7, < 2.0.11 |
2.0.11 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
getgrav/grav Packagist → 2.0.11
- composer
composer require getgrav/grav:^2.0.11
Severity breakdown
CVSS 4.0 8.6
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
- Attack vector
- Network
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- Low
- User interaction
- None
- Confidentiality (vulnerable system)
- High
- Integrity (vulnerable system)
- High
- Availability (vulnerable system)
- None
- Confidentiality (subsequent systems)
- None
- Integrity (subsequent systems)
- None
- Availability (subsequent systems)
- None
- Exploit maturity
- Not defined
- Scope
- X
CVSS 3.1 8.1
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Attack vector
- Network
- Attack complexity
- Low
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- High
- Availability
- None
Weakness
CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
How to prevent it
- Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.
CWE-470: Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')
The product uses external input with reflection to select which classes or code to use, but it does not sufficiently prevent the input from selecting improper classes or code.
How to prevent it
- Architecture and Design: Refactor your code to avoid using reflection.
- Architecture and Design: Do not use user-controlled inputs to select and load classes or code.
- Implementation: Apply strict input validation by using allowlists or indirect selection to ensure that the user is only selecting allowable classes or code.
CWE-94: Improper Control of Generation of Code ('Code Injection')
The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.
How to prevent it
- Architecture and Design: Refactor your program so that you do not have to dynamically generate code.
- Architecture and Design: Run your code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which code can be executed by your product. Examples include the Unix chroot jail and AppArmor. In general, managed code may provide some protection. This may not be a feasible solution, and it only limits the impact to the operating system; the rest of your application may still be subject to compromise. Be careful to avoid CWE-243 and other weaknesses related to jails.
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright. To reduce the likelihood of code injection, use stringent allowlists that limit which constructs are allowed. If you are dynamically constructing code that invokes a function, then verifying that the input is alphanumeric might be insufficient. An attacker might still be able to reference a dangerous function that you did not intend to allow, such as system(), exec(), or exit().
- Testing: Use dynamic tools and techniques that interact with the product using large test suites with many diverse inputs, such as fuzz testing (fuzzing), robustness testing, and fault injection. The product's operation may slow down, but it should not become unstable, crash, or generate incorrect results.
- Operation: Run the code in an environment that performs automatic taint propagation and prevents any command execution that uses tainted variables, such as Perl's "-T" switch. This will force the program to perform validation steps that remove the taint, although you must be careful to correctly validate your inputs so that you do not accidentally mark dangerous inputs as untainted (see CWE-183 and CWE-184).
- Operation: Run the code in an environment that performs automatic taint propagation and prevents any command execution that uses tainted variables, such as Perl's "-T" switch. This will force the program to perform validation steps that remove the taint, although you must be careful to correctly validate your inputs so that you do not accidentally mark dangerous inputs as untainted (see CWE-183 and CWE-184).
- Implementation: For Python programs, it is frequently encouraged to use the ast.literal_eval() function instead of eval, since it is intentionally designed to avoid executing code. However, an adversary could still cause excessive memory or stack consumption via deeply nested structures [REF-1372], so the python documentation discourages use of ast.literal_eval() on untrusted data [REF-1373].
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-xjw5-q542-3vmr Grav: config_denied_paths default list omits `system`, exposing real secrets (e.g. system.cache.redis.password) via the Twig sandbox when config_access is enabled | High | Also affects getgrav/grav |
| GHSA-3jhr-mxmx-38cx Grav: UserInterface offsetget/offsetexists allow-listed in Twig sandbox let editor-authored content leak hashed_password and 2FA secrets via offsetGet() | Medium | Also affects getgrav/grav |
| GHSA-9ccq-2jfg-qw33 Grav: Origin validation bypass in Uri::referrer() and Pages::referrerRoute() via unanchored prefix match | Critical | Also affects getgrav/grav |
| GHSA-38p6-h87p-r4cg Grav: Non constant time nonce comparison in Utils::verifyNonce() used for CSRF protection | Medium | Also affects getgrav/grav |
| GHSA-p597-crqc-m349 Grav: The system, site, and theme Twig variables bypass the content sandbox entirely and are never covered by config_denied_paths | Medium | Also affects getgrav/grav |
| GHSA-47ch-6w46-6xm7 Grav: media_directory() Twig function allows filesystem path traversal and file content disclosure from sandboxed page content | Medium | Also affects getgrav/grav |
| GHSA-jq29-c7v8-rg55 Grav: Path Traversal in MediaUploadTrait::deleteFile() Allows Arbitrary File Deletion | High | Also affects getgrav/grav |
| GHSA-8hgv-xc77-jmcr Grav: Page editors can inject arbitrary script into rendered pages via the Twig sandbox's assets.addJs/addCss allowlist, escalating to super-admin | Medium | Also affects getgrav/grav |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor