cbcvebase.
CVE-2026-45270
published 2026-05-18

CVE-2026-45270: CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule ## Summary The `Pages` backend module registers the `html_purify` validation…

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

```php
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:
```php
'lang.*.content' => ['label' => lang('Backend.content'), 'rules' => 'required|html_purify'],
```
Then at lines 102–113 it reads the raw POST and inserts it untouched:
```php
$langsData = $this->request->getPost('lang') ?? [];
...
$this->commonModel->create('page

Affected

1 ranges
VendorProductVersion rangeFixed in
ci4-cms-erpci4ms>= 0 < 0.31.9.00.31.9.0
Stop checking back — get the weekly exploitation signal.

Every Monday: what got weaponized or added to CISA KEV in the last seven days — each CVE cross-linked to its PoC, Nuclei template, and detection rule. Free, one email a week, unsubscribe in one click.