CVE-2026-45270
published 2026-05-18CVE-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('pageAffected
1 ranges
| Vendor | Product | Version range | Fixed in |
|---|---|---|---|
| ci4-cms-erp | ci4ms | >= 0 < 0.31.9.0 | 0.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.
No detection rules found.
No public exploits indexed.
No writeups or analysis indexed.
2026-05-18
Published