Command Injection Explained

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

Vulnerable: passing user input to exec()
// 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 execution
  • cmd or $(cmd) - Command substitution
  • \n - Newline (new command)

How to Prevent Command Injection

Safe: using spawn with argument array
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. sharp instead of convert, dns.resolve() instead of nslookup, node-ping instead of shelling out ping.

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.

Vulnerability Guides

Command Injection Explained