GitHub Advisoryadvisory · Packagist
GHSA-7cm9-v848-cfh2 CI4MS has stored XSS via Unescaped Blacklist Note in Admin User List
MEDIUM
Official page ↗
- Aliases
- CVE-2026-39391
- Ecosystem
- Packagist
- Packages
- ci4-cms-erp/ci4ms
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N
- Weaknesses
- CWE-79
- Reviewed
- 2026-04-08
Affected packages
• Packagist ci4-cms-erp/ci4ms: < 0.31.4.0 → fixed in 0.31.4.0
Description
Summary
The blacklist (ban) note parameter in `UserController::ajax_blackList_post()` is stored in the database without sanitization and rendered into an HTML `data-note` attribute without escaping. An admin with blacklist privileges can inject arbitrary JavaScript that executes in the browser of any other admin who views the user management page.
Details
In `modules/Users/Controllers/UserController.php`, the `ajax_blackList_post()` method (line 344-362) accepts a `note` POST parameter with only a `required` validation rule:
```php
// Line 347 — validation only checks 'required', no sanitization
$valData = (['note' => ['label' => lang('Backend.notes'), 'rules' => 'required'],
'uid' => ['label' => 'uid', 'rules' => 'required|is_natural_no_zero']]);
// Line 352 — raw user input passed directly to ban()
$user->ban($this->request->getPost('note'));
```
Shield's `Bannable::ban()` trait stores the message as-is:
```php
// vendor/codeigniter4/shield/src/Traits/Bannable.php
public function ban(?string $message = null): self
{
$this->status = 'banned';
$this->status_message = $message; // No escaping
// ...
}
```
In the `users()` method (line 13-91), when building the DataTables response, the `status_message` is concatenated directly into HTML without escaping:
```php
// Line 55 — esc() IS used here (correct)
$result->fullname = esc($result->firstname) . ' ' . esc($result->surname);
// Line 58-59 — NO esc() on status_message (vulnerable)
if ($result->status == 'banned'):
$result->actions .= 'status_message . '">'
```
The HTML string is returned as JSON (line 90) and DataTables renders it into the DOM. CSP is disabled (`$CSPEnabled = false` in `App.php`), and no `SecureHeaders` filter is applied.
PoC
Step 1 — Store XSS payload via ban endpoint:
```bash
curl -X POST 'https://TARGET/backend/users/blackList' \
-H 'X-Requested-With: XMLHttpRequest' \
-H 'Cookie: ci_session=ADMIN_SESSION_WITH_UPDATE_PERM' \
-d 'uid=2¬e=%22+onmouseover%3D%22alert(document.cookie)%22+x%3D%22'
```
Expected response: `{"result":true,"error":{"type":"success","message":"..."}}`
Step 2 — Trigger payload:
Any admin navigating to `/backend/users` will receive HTML containing:
```html
```
The XSS fires when the admin hovers over the blacklist button for the banned user.
Alternative immediate-execution payload:
```
note=">
```
Impact
• Session hijacking: An attacker with blacklist privileges can steal session cookies of other admins (including superadmins who view the user list but are themselves protected from being banned).
• Privilege escalation: A lower-privileged admin could use stolen superadmin sessions to gain full control.
• Persistent: The payload persists in the database and fires every time the user list is loaded, affecting all admins who view the page.
Recommended Fix
Wrap `status_message` with `esc()` to match the escaping already applied to other user fields on line 55:
```php
// In users() method, line 58-59 — change:
$result->actions .= 'id . '" data-status="' . $result->status . '" data-note="' . esc($result->status_message) . '"><i
```
Related
Alias · CVE (1)
- CVE-2026-39391 CI4MS is a CodeIgniter 4-based CMS skeleton that delivers a production-ready, modular architecture with RBAC authorization and theme… vulnerability
Weakness · CWE (1)
- CWE-79 Improper Neutralization of Input During Web Page Generation weakness