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

GHSA-XHFV-7758-R9HX

Vulnerability from github – Published: 2026-09-17 20:45 – Updated: 2026-09-17 20:45
VLAI
Summary
Grav: Missing admin.super guard on core group blueprint access field allows admin.users operator to escalate to super-admin
Details

Summary

The core Flex group blueprint system/blueprints/user/group.yaml (access field, lines 48-55) omits the security@: admin.super field guard that its sibling account blueprint carries (account.yaml:131/138/150, added by the CVE-2026-42613 fix). A delegated non-super operator holding admin.users.update can therefore save a group whose access map contains admin.super: true, which UserGroupObject::authorize then grants to every member of that group, a full privilege escalation to super-admin (scheduler/cron RCE, Twig eval). This is a distinct file, sink, and fix from all four related advisories.

Root Cause

The CVE-2026-42613 fix protected the account access/groups fields with a blueprint-level security@: admin.super gate, which Blueprint::dynamicSecurity() (system/src/Grav/Common/Data/Blueprint.php:644-662) uses to mark a field validate.ignore=true for non-super users so BlueprintSchema::filterArray() (BlueprintSchema.php:263-311) drops it. The functionally-identical group access field, a permission map granted to every member of the group, is declared in system/blueprints/user/group.yaml:48-55 with check_authorize: false and NO security@ guard. check_authorize has ZERO PHP enforcement (grep -rn check_authorize across the repo returns 0 PHP consumers; only two YAML blueprints reference it), so security@ is the sole real control. Because the group access field lacks security@, dynamicSecurity never flags it, filterArray retains it, and Validation::filterArray (type: array, value_type: bool) passes the nested admin.super:true leaf through. The core save path (FlexObject::update() then Framework/Flex/FlexObject.php:683 $blueprint->filter($data,true,true) then save()) persists it to user://config/groups.yaml.

Impact

A delegated admin.users operator (strictly below admin.super) escalates to super-admin, gaining the admin panel, the scheduler (cron to RCE) and Twig evaluation. Full read/write/DoS (C:H/I:H/A:H). Even absent self-escalation, arbitrary rewrite of ANY group's ACL is itself a full escalation primitive.

Proof of Concept

As a non-super admin.users operator who is a member of group ops:

POST /admin/accounts/groups/ops   (or groups.json task:save)
data[access][admin][super]=1

user/config/groups.yaml gains ops: { access: { admin: { super: true } } }, so the operator is super-admin on the next request.

Attack Chain

  1. Entry: authenticated delegated admin (admin.users.update, no admin.super) POSTs the group-edit form for a group they belong to (or a new group), body access[admin][super]=true. Guard: FlexAuthorizeTrait::isAuthorizedAction to admin.users.update. Bypass proof: user-groups.yaml exposes groups at admin.users:crudl; operator legitimately holds update.
  2. Check (field guard): Blueprint::dynamicSecurity marks validate.ignore only for security@ fields. Guard: none on group access (no security@). Bypass proof: group.yaml:48-55 has no security@; git log -S 'security@' -- system/blueprints/user/group.yaml is empty.
  3. Filter: BlueprintSchema::filterArray retains the non-ignored field; Validation::filterArray (value_type: bool) passes admin.super:true through. Bypass proof: field not flagged ignore/disabled; nested bool leaf preserved.
  4. Sink (persist): FlexObject::update then filter then save writes to user://config/groups.yaml. Guard: none. Bypass proof: value already survived steps 2/3; storage performs no ACL filtering.
  5. Impact: any member request triggers UserGroupObject::authorize('admin.super') (.../UserGroups/UserGroupObject.php:75) returning true, so the member is super-admin, enabling scheduler/Twig RCE.

Bypass Evidence

  • system/blueprints/user/group.yaml:48-55: access block with check_authorize: false, no security@ (confirmed live on tag 2.0.12).
  • git log -S 'security@' -- system/blueprints/user/group.yaml is empty (guard never existed; a permanent gap, not a regression).
  • git log 2.0.12..HEAD -- system/blueprints/user/group.yaml is empty (no post-release fix).
  • grep -rn check_authorize (whole repo) returns 0 PHP hits (guard unenforced in PHP).
  • grep -rn "typePermissions|filterPermissions" system/src/Grav/Common/Data returns 0 (permissions map falls through to array-bool filtering that keeps nested keys).
  • Guard asymmetry: account.yaml:131/138/150 carry security@: admin.super; group.yaml carries none. GHSA-h33v-82r9-v8pm's own text confirms the blueprint security@ gate is the sole Flex-backend strip for these keys.

Affected Versions

<= 2.0.12 (latest release; the guard never existed on group.yaml, so all 2.x are affected). The missing guard and the strip logic are both in core getgrav/grav; the group-edit UI is provided by the flex-objects/admin plugin, but the fix belongs in core.

Suggested Fix

Add security@: admin.super to the access field in system/blueprints/user/group.yaml (mirroring account.yaml), and/or enforce a super-only strip on group ACL saves in core so all callers are covered.


Reported by zx (Jace) — GitHub: @manus-use

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getgrav/grav"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.0.14"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-75837"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-269",
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-17T20:45:15Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\nThe core Flex group blueprint `system/blueprints/user/group.yaml` (access field, lines 48-55) omits the `security@: admin.super` field guard that its sibling account blueprint carries (`account.yaml:131/138/150`, added by the CVE-2026-42613 fix). A delegated non-super operator holding `admin.users.update` can therefore save a group whose `access` map contains `admin.super: true`, which `UserGroupObject::authorize` then grants to every member of that group, a full privilege escalation to super-admin (scheduler/cron RCE, Twig eval). This is a distinct file, sink, and fix from all four related advisories.\n\n## Root Cause\nThe CVE-2026-42613 fix protected the account `access`/`groups` fields with a blueprint-level `security@: admin.super` gate, which `Blueprint::dynamicSecurity()` (`system/src/Grav/Common/Data/Blueprint.php:644-662`) uses to mark a field `validate.ignore=true` for non-super users so `BlueprintSchema::filterArray()` (`BlueprintSchema.php:263-311`) drops it. The functionally-identical group `access` field, a permission map granted to every member of the group, is declared in `system/blueprints/user/group.yaml:48-55` with `check_authorize: false` and NO `security@` guard. `check_authorize` has ZERO PHP enforcement (`grep -rn check_authorize` across the repo returns 0 PHP consumers; only two YAML blueprints reference it), so `security@` is the sole real control. Because the group access field lacks `security@`, `dynamicSecurity` never flags it, `filterArray` retains it, and `Validation::filterArray` (`type: array, value_type: bool`) passes the nested `admin.super:true` leaf through. The core save path (`FlexObject::update()` then `Framework/Flex/FlexObject.php:683` `$blueprint-\u003efilter($data,true,true)` then `save()`) persists it to `user://config/groups.yaml`.\n\n## Impact\nA delegated `admin.users` operator (strictly below `admin.super`) escalates to super-admin, gaining the admin panel, the scheduler (cron to RCE) and Twig evaluation. Full read/write/DoS (C:H/I:H/A:H). Even absent self-escalation, arbitrary rewrite of ANY group\u0027s ACL is itself a full escalation primitive.\n\n## Proof of Concept\nAs a non-super `admin.users` operator who is a member of group `ops`:\n```\nPOST /admin/accounts/groups/ops   (or groups.json task:save)\ndata[access][admin][super]=1\n```\n`user/config/groups.yaml` gains `ops: { access: { admin: { super: true } } }`, so the operator is super-admin on the next request.\n\n## Attack Chain\n1. Entry: authenticated delegated admin (`admin.users.update`, no `admin.super`) POSTs the group-edit form for a group they belong to (or a new group), body `access[admin][super]=true`. Guard: `FlexAuthorizeTrait::isAuthorizedAction` to `admin.users.update`. Bypass proof: `user-groups.yaml` exposes groups at `admin.users:crudl`; operator legitimately holds update.\n2. Check (field guard): `Blueprint::dynamicSecurity` marks `validate.ignore` only for `security@` fields. Guard: none on group access (no `security@`). Bypass proof: `group.yaml:48-55` has no `security@`; `git log -S \u0027security@\u0027 -- system/blueprints/user/group.yaml` is empty.\n3. Filter: `BlueprintSchema::filterArray` retains the non-ignored field; `Validation::filterArray` (`value_type: bool`) passes `admin.super:true` through. Bypass proof: field not flagged ignore/disabled; nested bool leaf preserved.\n4. Sink (persist): `FlexObject::update` then `filter` then `save` writes to `user://config/groups.yaml`. Guard: none. Bypass proof: value already survived steps 2/3; storage performs no ACL filtering.\n5. Impact: any member request triggers `UserGroupObject::authorize(\u0027admin.super\u0027)` (`.../UserGroups/UserGroupObject.php:75`) returning true, so the member is super-admin, enabling scheduler/Twig RCE.\n\n## Bypass Evidence\n- `system/blueprints/user/group.yaml:48-55`: access block with `check_authorize: false`, no `security@` (confirmed live on tag 2.0.12).\n- `git log -S \u0027security@\u0027 -- system/blueprints/user/group.yaml` is empty (guard never existed; a permanent gap, not a regression).\n- `git log 2.0.12..HEAD -- system/blueprints/user/group.yaml` is empty (no post-release fix).\n- `grep -rn check_authorize` (whole repo) returns 0 PHP hits (guard unenforced in PHP).\n- `grep -rn \"typePermissions|filterPermissions\" system/src/Grav/Common/Data` returns 0 (permissions map falls through to array-bool filtering that keeps nested keys).\n- Guard asymmetry: `account.yaml:131/138/150` carry `security@: admin.super`; `group.yaml` carries none. GHSA-h33v-82r9-v8pm\u0027s own text confirms the blueprint `security@` gate is the sole Flex-backend strip for these keys.\n\n## Affected Versions\n`\u003c= 2.0.12` (latest release; the guard never existed on `group.yaml`, so all 2.x are affected). The missing guard and the strip logic are both in core `getgrav/grav`; the group-edit UI is provided by the flex-objects/admin plugin, but the fix belongs in core.\n\n## Suggested Fix\nAdd `security@: admin.super` to the `access` field in `system/blueprints/user/group.yaml` (mirroring `account.yaml`), and/or enforce a super-only strip on group ACL saves in core so all callers are covered.\n\n---\nReported by **zx (Jace)** \u2014 GitHub: @manus-use",
  "id": "GHSA-xhfv-7758-r9hx",
  "modified": "2026-09-17T20:45:15Z",
  "published": "2026-09-17T20:45:15Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/security/advisories/GHSA-xhfv-7758-r9hx"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-75837"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/getgrav/grav"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/grav-before-privilege-escalation-via-group-access-field"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Grav: Missing admin.super guard on core group blueprint access field allows admin.users operator to escalate to super-admin"
}



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…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…