{"id":"GHSA-c2q3-p4jr-c55f","summary":"Grav Vulnerable to XSS via Taxonomy Field Values in Admin Panel","details":"### Summary\n\nA Stored Cross-Site Scripting (XSS) vulnerability exists in the Grav CMS Form plugin's select field template. Taxonomy tag and category values are rendered with the Twig `|raw` filter in the admin panel, bypassing the global autoescape protection. An editor-level user can inject arbitrary JavaScript that executes in any administrator's browser session when they view or edit any page in the admin panel.\n\nAdditionally, Grav's built-in XSS detection (`Security::detectXss()`) can be bypassed by using payloads that close the `\u003coption\u003e/\u003cselect\u003e` context and use unquoted event handlers - the `on_events` regex fails to match event handlers without quotes or trailing spaces before `\u003e`.\n\n### Important\n\n- The vulnerability is in the Form plugin (`select.html.twig`), which is installed by default with Grav\n- The XSS is cross-page: a malicious taxonomy value on one page executes when an admin edits any page, because taxonomy options are rendered from a shared global pool\n- An editor can exploit this without any other vulnerability - taxonomy fields are not in the server-side restricted fields list\n- The `HttpOnly` flag on session cookies prevents direct session theft, but the XSS can steal the admin nonce and perform privileged actions via JavaScript\n\n### Permissions Needed\n\n- Editor: can create or edit pages and set taxonomy tag/category values\n\n### Details\n\nThe Form plugin's select field template renders option values using the `|raw` Twig filter, which outputs content without HTML escaping:\n\nFile: `user/plugins/form/templates/forms/fields/select/select.html.twig`\n\n```twig\n{# Line 55 #}\n avalue|raw \n\n{# Line 65 #}\n suboption|t|raw \n\n{# Line 72 #}\n item_value|t|raw \n```\n\nThe taxonomy field in the page editor uses this select template. When a page has taxonomy values (tags, categories), these values are populated as `\u003coption\u003e` elements in the select dropdown. The `value` attribute is properly escaped by the browser's attribute encoding, but the **display text** between `\u003coption\u003e` tags is rendered raw:\n\n```html\n\u003coption value=\"&lt;script&gt;alert(1)&lt;/script&gt;\"\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/option\u003e\n```\n\nSince taxonomy options are collected globally across all pages (to provide autocomplete/selection), a malicious taxonomy value on any page will appear in the taxonomy dropdown of every page editor - making this a cross-page stored XSS.\n\nThe server-side field restriction in the flex-objects plugin only blocks `['form', 'forms', 'process', 'twig']` for non-super users. Taxonomy fields are not restricted, so editors can freely set arbitrary taxonomy values.\n\n### XSS Detection Bypass\n\nGrav's `Security::detectXss()` checks for `dangerous_tags` (e.g., `\u003cscript\u003e`, `\u003ciframe\u003e`), `on_events` (event handlers), and `invalid_protocols` (e.g., `javascript:`). However, the `on_events` regex:\n\n```php\n'on_events' =\u003e '#(\u003c[^\u003e]+[a-z\\x00-\\x20\"\\'\\/)(?:on[a-z]+)\\s*=[\\s|\\'\"'].*[\\s|\\'\"']\u003e#iUu'\n```\n\nrequires either quotes around the handler value or a trailing space before `\u003e`. An unquoted handler like `onerror=alert(1)\u003e` (no space before `\u003e`) bypasses this check entirely.\n\nCombined with `\u003c/option\u003e\u003c/select\u003e` to break out of the select context (neither tag is in `dangerous_tags`), the full payload evades all three detection layers and triggers no XSS warning in the admin panel.\n\n### PoC\n\n#### Step 1: Login as Editor\nNavigate to `http://TARGET/admin/` and authenticate with editor credentials.\n\n#### Step 2: Create a Page with Malicious Taxonomy\n- Go to Pages → Add → Add Page\n- Title: `XSS via editor`\n- Go to **Options** Tap\n- On Taxonomies, Add tag:\n```\n\u003c/option\u003e\u003c/select\u003e\u003cimg src=x onerror=alert('XSS-via-editor')\u003e\n```\n\nThis payload:\n- Closes `\u003c/option\u003e\u003c/select\u003e` to break out of the select dropdown context\n- Injects an `\u003cimg\u003e` tag with an unquoted `onerror` handler (bypasses `on_events` regex)\n- Is not in the `dangerous_tags` list (no `\u003cscript\u003e`, `\u003ciframe\u003e`, etc.)\n- Triggers no XSS warning in the admin panel\n\n\u003cimg width=\"1221\" height=\"857\" alt=\"image\" src=\"https://github.com/user-attachments/assets/6223cbb2-f04b-46bd-89ce-828c89ad77ab\" /\u003e\n\n#### Step 3: Trigger the XSS\nWhen any administrator navigates to the page editor of any page (not just the malicious one), the JavaScript executes immediately.\n\n\u003cimg width=\"1224\" height=\"856\" alt=\"image\" src=\"https://github.com/user-attachments/assets/f008b0f2-dedb-4b22-a74a-cdc0d7325cb4\" /\u003e\n\nThe XSS fires because taxonomy tag options are collected globally across all pages and rendered with `|raw` in the select dropdown template. The payload breaks out of the `\u003coption\u003e` context, and the browser renders the `\u003cimg\u003e` tag as a regular DOM element.\n\n### Impact\n\n- Session hijacking: While `HttpOnly` prevents direct cookie theft, the XSS can steal the admin nonce token and perform any admin action via AJAX requests\n- Privilege escalation: An editor can perform admin-only actions (create users, modify system configuration, install plugins) through the hijacked admin session\n- Cross-page impact: A single malicious taxonomy value affects the entire admin panel - every page editor view is compromised\n\n\n---\n\n## Maintainer note — fix applied (2026-04-24)\n\nFixed across two repos:\n\n1. **grav-plugin-form 9.0.1** (commit [`6bffb4c`](https://github.com/getgrav/grav-plugin-form/commit/6bffb4c)) — the primary fix. All four `|raw` filters in [`templates/forms/fields/select/select.html.twig`](https://github.com/getgrav/grav-plugin-form/blob/develop/templates/forms/fields/select/select.html.twig) (placeholder, avalue, suboption, item_value) have been removed. Option labels — including taxonomy values that propagate cross-page through the admin's shared selection pool — now go through Twig's default escaper, so a lower-privileged editor can no longer inject script that runs in an admin's browser when they open any page editor.\n\n2. **Grav core on the `2.0` branch** (commit [`5a12f9be8`](https://github.com/getgrav/grav/commit/5a12f9be8), ships in **2.0.0-beta.2**) — closes the detection-bypass half of the report. The `on_events` regex in `Security::detectXss()` is tightened so unquoted handlers like `onerror=alert(1)\u003e` are flagged (see separate GHSA-9695-8fr9-hw5q), and `option`/`select` have been added to default `security.xss_dangerous_tags` so `\u003c/option\u003e\u003c/select\u003e…` tripwires the detector (see separate GHSA-w8cg-7jcj-4vv2).\n\nSites running admin2 on Grav 2.0.0-beta.2 get the 9.0.1 form plugin automatically via its existing dependency graph.\n\n**Files:**\n- [`templates/forms/fields/select/select.html.twig`](https://github.com/getgrav/grav-plugin-form/blob/develop/templates/forms/fields/select/select.html.twig) — four `|raw` removed.\n- [`system/config/security.yaml`](https://github.com/getgrav/grav/blob/2.0/system/config/security.yaml) — dangerous-tags list extended.\n- [`system/src/Grav/Common/Security.php`](https://github.com/getgrav/grav/blob/2.0/system/src/Grav/Common/Security.php) — `on_events` regex tightened.\n- [`tests/unit/Grav/Common/Security/DetectXssTest.php`](https://github.com/getgrav/grav/blob/2.0/tests/unit/Grav/Common/Security/DetectXssTest.php) — includes the GHSA-c2q3 PoC payload.","aliases":["CVE-2026-42842"],"modified":"2026-09-10T03:50:46.554857469Z","published":"2026-05-05T21:24:45Z","database_specific":{"nvd_published_at":"2026-05-11T17:16:33Z","cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-05-05T21:24:45Z"},"references":[{"type":"WEB","url":"https://github.com/getgrav/grav/security/advisories/GHSA-c2q3-p4jr-c55f"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-42842"},{"type":"WEB","url":"https://github.com/getgrav/grav-plugin-form/commit/6bffb4c98be468a155d1656544ec45bb4a443957"},{"type":"WEB","url":"https://github.com/getgrav/grav/commit/5a12f9be8314682c8713e569e330f11805d0a663"},{"type":"PACKAGE","url":"https://github.com/getgrav/grav"}],"affected":[{"package":{"name":"getgrav/grav","ecosystem":"Packagist","purl":"pkg:composer/getgrav/grav"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.0.0-beta.2"}]}],"versions":["0.8.0","0.9.0","0.9.1","0.9.10","0.9.11","0.9.12","0.9.13","0.9.14","0.9.15","0.9.16","0.9.17","0.9.18","0.9.19","0.9.2","0.9.20","0.9.21","0.9.22","0.9.23","0.9.24","0.9.25","0.9.26","0.9.27","0.9.28","0.9.29","0.9.3","0.9.30","0.9.31","0.9.32","0.9.33","0.9.34","0.9.35","0.9.36","0.9.37","0.9.38","0.9.39","0.9.4","0.9.40","0.9.41","0.9.42","0.9.43","0.9.44","0.9.45","0.9.5","0.9.6","0.9.7","0.9.8","0.9.9","1.0.0","1.0.0-rc.1","1.0.0-rc.2","1.0.0-rc.3","1.0.0-rc.4","1.0.0-rc.5","1.0.0-rc.6","1.0.1","1.0.10","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8","1.0.9","1.1.0","1.1.0-beta.1","1.1.0-beta.2","1.1.0-beta.3","1.1.0-beta.4","1.1.0-beta.5","1.1.0-rc.1","1.1.0-rc.2","1.1.0-rc.3","1.1.1","1.1.10","1.1.11","1.1.12","1.1.13","1.1.14","1.1.15","1.1.16","1.1.17","1.1.2","1.1.3","1.1.4","1.1.5","1.1.6","1.1.7","1.1.8","1.1.9","1.1.9-rc.1","1.1.9-rc.2","1.1.9-rc.3","1.2.0","1.2.0-rc.1","1.2.0-rc.2","1.2.0-rc.3","1.2.1","1.2.2","1.2.3","1.2.4","1.3.0","1.3.0-rc.1","1.3.0-rc.2","1.3.0-rc.3","1.3.0-rc.4","1.3.0-rc.5","1.3.1","1.3.10","1.3.2","1.3.3","1.3.4","1.3.5","1.3.6","1.3.7","1.3.8","1.3.9","1.4.0","1.4.0-beta.1","1.4.0-beta.2","1.4.0-beta.3","1.4.0-rc.1","1.4.0-rc.2","1.4.1","1.4.2","1.4.3","1.4.4","1.4.5","1.4.6","1.4.7","1.4.8","1.5.0","1.5.0-beta.1","1.5.0-beta.2","1.5.0-rc.1","1.5.1","1.5.10","1.5.2","1.5.3","1.5.4","1.5.5","1.5.6","1.5.7","1.5.8","1.5.9","1.6.0","1.6.0-beta.1","1.6.0-beta.2","1.6.0-beta.3","1.6.0-beta.4","1.6.0-beta.5","1.6.0-beta.6","1.6.0-beta.7","1.6.0-beta.8","1.6.0-rc.1","1.6.0-rc.2","1.6.0-rc.3","1.6.0-rc.4","1.6.1","1.6.10","1.6.11","1.6.12","1.6.13","1.6.14","1.6.15","1.6.16","1.6.17","1.6.18","1.6.19","1.6.2","1.6.20","1.6.21","1.6.22","1.6.23","1.6.24","1.6.25","1.6.26","1.6.27","1.6.28","1.6.29","1.6.3","1.6.30","1.6.31","1.6.4","1.6.5","1.6.6","1.6.7","1.6.8","1.6.9","1.7.0","1.7.0-beta.1","1.7.0-beta.10","1.7.0-beta.2","1.7.0-beta.3","1.7.0-beta.4","1.7.0-beta.5","1.7.0-beta.6","1.7.0-beta.7","1.7.0-beta.8","1.7.0-beta.9","1.7.0-rc.1","1.7.0-rc.10","1.7.0-rc.11","1.7.0-rc.12","1.7.0-rc.13","1.7.0-rc.14","1.7.0-rc.15","1.7.0-rc.16","1.7.0-rc.17","1.7.0-rc.18","1.7.0-rc.19","1.7.0-rc.2","1.7.0-rc.20","1.7.0-rc.3","1.7.0-rc.4","1.7.0-rc.5","1.7.0-rc.6","1.7.0-rc.7","1.7.0-rc.8","1.7.0-rc.9","1.7.1","1.7.10","1.7.12","1.7.13","1.7.14","1.7.15","1.7.16","1.7.17","1.7.18","1.7.19","1.7.20","1.7.21","1.7.22","1.7.23","1.7.24","1.7.25","1.7.26","1.7.26.1","1.7.27","1.7.27.1","1.7.28","1.7.29","1.7.29.1","1.7.3","1.7.30","1.7.31","1.7.32","1.7.33","1.7.34","1.7.35","1.7.36","1.7.37","1.7.37.1","1.7.38","1.7.39","1.7.39.1","1.7.39.2","1.7.39.3","1.7.39.4","1.7.4","1.7.40","1.7.41","1.7.41.1","1.7.41.2","1.7.42","1.7.42.1","1.7.42.2","1.7.42.3","1.7.43","1.7.44","1.7.45","1.7.46","1.7.47","1.7.48","1.7.49","1.7.49.1","1.7.49.2","1.7.49.3","1.7.49.4","1.7.49.5","1.7.5","1.7.51","1.7.52","1.7.53","1.7.53.1","1.7.53.2","1.7.53.3","1.7.6","1.7.7","1.7.8","1.7.9","1.8.0-beta.1","1.8.0-beta.10","1.8.0-beta.11","1.8.0-beta.12","1.8.0-beta.13","1.8.0-beta.14","1.8.0-beta.15","1.8.0-beta.16","1.8.0-beta.17","1.8.0-beta.18","1.8.0-beta.19","1.8.0-beta.2","1.8.0-beta.20","1.8.0-beta.21","1.8.0-beta.22","1.8.0-beta.23","1.8.0-beta.24","1.8.0-beta.25","1.8.0-beta.26","1.8.0-beta.27","1.8.0-beta.28","1.8.0-beta.29","1.8.0-beta.3","1.8.0-beta.4","1.8.0-beta.5","1.8.0-beta.6","1.8.0-beta.7","1.8.0-beta.8","1.8.0-beta.9","2.0.0-beta.1"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-c2q3-p4jr-c55f/GHSA-c2q3-p4jr-c55f.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N"}]}