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