All vulnerabilities
GHSA-47gg-mh38-9rr7

getgrav/grav: Page editor can still call arbitrary static methods through a Flex form override

getgrav/grav
CVSS
n/aHigh
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in2.2.13Fixed versionGitHub
Published24 Oct 2026Updated 24 Oct 2026GitHub

What fixes it

  • 2.2.13getgrav/grav, composerGitHubcomposer require "getgrav/grav:^2.2.13"

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

The fix for GHSA-9rq7-4cch-f22j in Grav 2.2.2 was incomplete. A page editor can still get a data-*@ directive in a page-authored Flex form past Grav's allowed-callable check, either by renaming the field with unset-name@ instead of replace-name@, or by giving the field an integer key. The moved field is treated as if it came from the trusted blueprint file, so the directive can call any public static method, for example Folder::delete, Folder::move, Folder::copy or Utils::download.

Affected versions

Grav 2.0.15 through 2.2.12. 2.0.15 is when directives from file blueprints started skipping the allowed-callable check; 2.2.2 closed only the replace-name@ spelling. 1.7 is not affected.

Details

Since 2.0.15, directives from file blueprints are trusted and directives from page-authored forms are held to a Class::method allowlist. Blueprint::extend() decides which incoming fields are untrusted by reading the incoming form as written, then merges it into the trusted blueprint. Any directive the merge puts at a different path is never marked untrusted, so Blueprint::init() gives it the base blueprint's trust and skips the allowlist.

2.2.2 removed replace-name@ from untrusted input before the merge. The merge in rockettheme/toolbox (BlueprintForm::deepMerge()) handles unset and replace in the same branch, so unset-name@ (also written @unset-name@ or unset-name@2) moves the field, directive included, to a new top-level key in exactly the same way. A second route needs no merge action at all: the merge adds a key the base does not have with array_merge(), which renumbers integer keys, so an override field keyed 5 lands at 0 and loses its untrusted attribution.

Both Flex form builders (FlexForm, FlexDirectoryForm) extend a file-defined directory blueprint with a form taken from page frontmatter, which is the reachable path. The directive runs when the page's form is built, for example when an administrator opens the page on the site.

Impact

An account with page-edit permission (api.pages.write, or admin.pages in the classic admin) can call server-side methods its role does not grant, giving arbitrary directory deletion, file move or overwrite, and file read outside the page tree.

Patches

Fixed in Grav 2.2.13. unset-name@ is now removed from untrusted input along with replace-name@, and after every merge of untrusted input Grav compares the merged directives with the ones the blueprint had before. Any directive that is new or changed is treated as untrusted, wherever the merge put it.

Credits

Reported by @soquip.

Text from GitHub Advisory Database

Affected versions 1

  • getgrav/grav, composer>= 2.0.15, < 2.2.13GitHub