CI4MS: Stored XSS in Pages Module Content via Broken html_purify Validation Rule
Date: 2026-05
Affected: composer/ci4-cms-erp/ci4ms
Attack vector: xss
Category: vulnerability-disclosure
Severity: High
CVE: CVE-2026-45138
CVE-2026-45270
CWE: CWE-79
CVSS: 8.7 (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N)

## 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(...)` afte


Mitigations:

Architectural layers (MAESTRO):