TL;DR
Send host=google.com; cat /etc/passwd to a /ping endpoint and you've handed over your server. That's command injection: user input flowing straight into exec() without sanitization. Don't use exec() or system() with user input. Use spawn() with an argument array. It bypasses the shell entirely, so ;, |, and && never run.
How Command Injection Works
// User wants to ping a host
app.get('/ping', (req, res) => {
const host = req.query.host;
exec(`ping -c 1 ${host}`, (error, stdout) => {
res.send(stdout);
});
});
// Attacker sends: host=example.com; cat /etc/passwd
// Executed: ping -c 1 example.com; cat /etc/passwd
Full server access: Command injection doesn't just leak data. It gives attackers a shell. They can read any file, install a backdoor, and pivot to your database or internal network from there.
Injection Characters
;- Command separator|- Pipe to another command&&/||- Conditional executioncmdor$(cmd)- Command substitution\n- Newline (new command)
How to Prevent Command Injection
import { spawn } from 'child_process';
app.get('/ping', (req, res) => {
const host = req.query.host;
// Validate input
if (!/^[a-zA-Z0-9.-]+$/.test(host)) {
return res.status(400).send('Invalid host');
}
// Use spawn with arguments array (no shell!)
const ping = spawn('ping', ['-c', '1', host]);
ping.stdout.on('data', (data) => res.write(data));
ping.on('close', () => res.end());
});
Prevention Rules
- Avoid
exec()entirely. Most tasks you're tempted to shell out for (file operations, image processing, network checks) have Node.js library equivalents that don't touch the shell. - When you must run a command, use
spawn()with an argument array. Arguments passed as an array never go through a shell interpreter, so injection characters are treated as literals, not operators. - Validate input before it goes anywhere. A strict allowlist (alphanumerics, dots, hyphens for a hostname) is a useful second layer, but it's not a substitute for the above two.
- Use library APIs over command-line tools.
sharpinstead ofconvert,dns.resolve()instead ofnslookup,node-pinginstead of shelling outping.
Is escaping shell characters enough?
No, and it's not close. Escaping is fragile: different shells, encodings, and edge cases catch people out constantly. The argument array approach doesn't escape at all: it bypasses the shell entirely, so there's nothing to escape. If you can't switch away from exec(), use a well-tested escaping library, but know you're fighting a losing battle long-term.
What about Windows commands?
Windows has different shell syntax (&, |, ^ behave differently and cmd.exe has its own quirks), but the attack class is identical. The fix is identical too: argument arrays, not string interpolation.
Scan for Injection Vulnerabilities
Our scanner tests for command injection patterns in your code.