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

CVE-2026-45138: CI4MS: Stored XSS in Blog Content via Broken `html_purify` Validation Rule ## Summary The custom `html_purify` validation rule used to sanitize blog post…

medium
CI4MS: Stored XSS in Blog Content via Broken `html_purify` Validation Rule

## Summary

The custom `html_purify` validation rule used to sanitize blog post bodies relies on by-reference mutation (`?string &$str`), but CodeIgniter 4's validator passes a local copy of the value, so the sanitized text is silently discarded. The Blog controller writes `$lanData['content']` directly into `blog_langs.content`, and the public template echoes it without escaping — yielding stored XSS executable in any visitor's browser, including the superadmin when previewing or editing posts.

## Details

### Root cause: by-reference mutation never propagates

`Modules\Backend\Validation\CustomRules::html_purify` declares its first argument by reference:

```php
// modules/Backend/Validation/CustomRules.php:54-73
public function html_purify(?string &$str = null, ?string &$error = null): bool
{
if (empty(trim((string)$str))) return true;
if (!class_exists('\HTMLPurifier')) { $error = lang('Backend.htmlPurifierNotFound'); return false; }
$clean = self::sanitizeHtml($str);
$str = $clean; // data`:

```php
// vendor/codeigniter4/framework/system/Validation/Validation.php:204-211
foreach ($values as $dotField => $value) { // local $value
$this->processRules($dotField, $setup['label'] ?? $field, $value, $rules, $data, $field);
}

// Validation.php:343-345
$passed = ($param === null)
? $set->{$rule}($value, $error) // {$rule}($value, $param, $data, $error, $field);
```

The reference mutation modifies that local `$value` only; `$this->data`, `$_POST`, and `getValidated()` keep the raw payload. The optional `getClean($original)` cache lookup in CustomRules.php:85-93 also fails because the cache was keyed on `md5(clean)` rather than `md5(original)`.

### Sink: raw POST is persisted and rendered unescaped

The Blog controller takes `$_POST['lang']` verbatim, runs it through validation (which always returns true for `html_purify`), and writes it to the database with no further filtering:

```php
// 

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.