Injection and output

Avoid command injection

Prefer library APIs, pass fixed executable arguments without a shell, and allowlist the small set of operations a feature supports.

Shells turn strings into programs. A shell reads characters like ;, |, &&, and backticks as instructions. Passing user input through a shell gives that input the power to start new commands.

The vulnerable shape passes a whole string to the shell to parse.

const { exec } = require('node:child_process')
// Vulnerable: the filename is interpreted by /bin/sh
exec(`convert uploads/${name} output.png`)

An image tool runs convert uploads/${name} output.png through a shell. A filename containing shell syntax starts a second command with the server’s permissions.

Pass arguments, not a command line

Prefer a native library when one exists. If you must spawn a process, choose the executable in code and pass arguments as a separate array. execFile does not invoke a shell, so shell metacharacters in an argument are just characters.

const { execFile } = require('node:child_process')
// Safe: fixed executable, arguments never parsed by a shell
execFile('convert', ['--', `uploads/${name}`, 'output.png'], (err) => {
  if (err) return handleError(err)
})

The -- marks the end of options, so a filename like -delete cannot be read as a flag. Combine this with an allowlist of the operations the feature actually supports.

Hand-written escaping changes across shells and platforms, and one missed character is enough. A library call or direct argument array removes shell parsing from the boundary.

Run the conversion through a no-shell process API with a fixed executable and argument list. Test spaces, quotes, separators, and a leading dash in the filename, then prove only the intended output file changes.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →