{"id":"GHSA-7cm9-v848-cfh2","aliases":["CVE-2026-39391"],"title":"CI4MS has stored XSS via Unescaped Blacklist Note in Admin User List","titleSource":"OSV","descriptionSource":"OSV","declaredSeverity":"MODERATE","description":"## Summary\n\nThe 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.\n\n## Details\n\nIn `modules/Users/Controllers/UserController.php`, the `ajax_blackList_post()` method (line 344-362) accepts a `note` POST parameter with only a `required` validation rule:\n\n```php\n// Line 347 — validation only checks 'required', no sanitization\n$valData = (['note' => ['label' => lang('Backend.notes'), 'rules' => 'required'],\n             'uid' => ['label' => 'uid', 'rules' => 'required|is_natural_no_zero']]);\n\n// Line 352 — raw user input passed directly to ban()\n$user->ban($this->request->getPost('note'));\n```\n\nShield's `Bannable::ban()` trait stores the message as-is:\n```php\n// vendor/codeigniter4/shield/src/Traits/Bannable.php\npublic function ban(?string $message = null): self\n{\n    $this->status         = 'banned';\n    $this->status_message = $message;  // No escaping\n    // ...\n}\n```\n\nIn the `users()` method (line 13-91), when building the DataTables response, the `status_message` is concatenated directly into HTML without escaping:\n\n```php\n// Line 55 — esc() IS used here (correct)\n$result->fullname = esc($result->firstname) . ' ' . esc($result->surname);\n\n// Line 58-59 — NO esc() on status_message (vulnerable)\nif ($result->status == 'banned'):\n    $result->actions .= '<button ... data-note=\"' . $result->status_message . '\">'\n```\n\nThe 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.\n\n## PoC\n\n**Step 1 — Store XSS payload via ban endpoint:**\n```bash\ncurl -X POST 'https://TARGET/backend/users/blackList' \\\n  -H 'X-Requested-With: XMLHttpRequest' \\\n  -H 'Cookie: ci_session=ADMIN_SESSION_WITH_UPDATE_PERM' \\\n  -d 'uid=2&note=%22+onmouseover%3D%22alert(document.cookie)%22+x%3D%22'\n```\n\nExpected response: `{\"result\":true,\"error\":{\"type\":\"success\",\"message\":\"...\"}}`\n\n**Step 2 — Trigger payload:**\nAny admin navigating to `/backend/users` will receive HTML containing:\n```html\n<button ... data-note=\"\" onmouseover=\"alert(document.cookie)\" x=\"\">\n```\n\nThe XSS fires when the admin hovers over the blacklist button for the banned user.\n\n**Alternative immediate-execution payload:**\n```\nnote=\"><img src=x onerror=alert(document.cookie)>\n```\n\n## Impact\n\n- **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).\n- **Privilege escalation**: A lower-privileged admin could use stolen superadmin sessions to gain full control.\n- **Persistent**: The payload persists in the database and fires every time the user list is loaded, affecting all admins who view the page.\n\n## Recommended Fix\n\nWrap `status_message` with `esc()` to match the escaping already applied to other user fields on line 55:\n\n```php\n// In users() method, line 58-59 — change:\n$result->actions .= '<button type=\"button\" class=\"btn btn-outline-dark btn-sm open-blacklist-modal\"\n                        data-id=\"' . $result->id . '\" data-status=\"' . $result->status . '\" data-note=\"' . esc($result->status_message) . '\"><i\n```","severity":"medium","score":4.8,"published":"2026-04-08T19:15:32.000Z","modified":"2026-04-08T19:41:25.362Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"ci4-cms-erp/ci4ms","sources":[{"name":"OSV · GHSA-7cm9-v848-cfh2","url":"https://osv.dev/vulnerability/GHSA-7cm9-v848-cfh2"}],"ranges":[{"events":[{"introduced":"0"},{"fixed":"0.31.4.0"}],"type":"ECOSYSTEM"}],"versions":["0.21.0","0.21.1","0.21.2","0.21.3","0.21.3.1","0.21.3.2","0.21.3.3","0.21.3.4","0.21.3.5","0.21.3.6","0.21.3.7","0.23.0.0","0.23.0.1","0.23.0.2","0.23.1.0","0.24.0.0","0.24.0.16","0.24.0.18","0.24.0.19","0.24.0.20","0.24.0.27","0.24.0.42","0.24.0.45","0.24.0.60","0.25.0.0","0.25.0.1","0.25.0.2","0.25.0.30","0.25.0.39","0.25.0.43","0.25.1.0","0.25.2.0","0.25.3.0","0.26.0.0","0.26.1.0","0.26.2.0","0.26.3.0","0.26.3.1","0.26.3.2","0.26.3.3","0.26.3.4","0.27.0.0","0.28.0.0","0.28.3.0","0.28.4.0","0.28.5.0","0.28.6.0","0.31.0.0","0.31.1.0","0.31.2.0","0.31.3.0"],"fixed":["0.31.4.0"]}],"sourceAffected":[],"references":["https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-7cm9-v848-cfh2","https://nvd.nist.gov/vuln/detail/CVE-2026-39391","https://github.com/ci4-cms-erp/ci4ms","https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0"],"sources":[{"name":"OSV","id":"GHSA-7cm9-v848-cfh2","modified":"2026-04-08T19:41:25.362010Z","url":"https://osv.dev/vulnerability/GHSA-7cm9-v848-cfh2"}],"assessments":[{"source":"OSV","vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N","score":4.8}],"products":[],"remediation":[],"kev":{"status":"not-listed","checkedAt":"2026-10-28T21:55:33.923Z"},"coverage":[{"source":"NVD","status":"not-checked"},{"source":"GHSA","status":"not-checked"},{"source":"EUVD","status":"not-checked"},{"source":"EPSS","status":"not-checked"}],"evidence":[]}