This document provides tips to avoid shell injection beyond using a shell escape library like Shescape. Most tips apply outside of Node.js, but some are specific to Node.js.
Please open an issue if you found a mistake or if you have a suggestion for how to improve the documentation.
This section provides tips for what you can do to protect against shell injection in addition to, or instead of, using Shescape.
Instead of using user input directly, evaluate the user's input and select an appropriate known safe value.
import { exec } from "node:child_process";
import { Shescape } from "shescape";
const shescape = new Shescape();
const userInput = "Yes";
// Good
exec(`echo 'Your choice was' ${shescape.quote(choice)}`);
// Better
let safeChoice;
switch (choice.toLowerCase()) {
case "yes":
case "y":
safeChoice = "yes";
break;
case "no":
case "n":
safeChoice = "no";
break;
default:
throw new Error(`Invalid choice '${choice}'`);
}
exec(`echo 'Your choice was ${safeChoice}'`);
Most shells allow you to use environment variables in your commands (e.g.
$PATH
or %PATH%
). Because of how environment variables are evaluated they
can prevent shell injection when using user input.
However, using environment variables is not without drawbacks:
- You must be careful not to leak any environment variables unintentionally.
- You may need to prevent argument splitting by quoting the environment variable.
- Environment variables must be a string and have a limited character set, if you assign an invalid value Node.js will throw an error.
- How environment variables are accessed differs between shells, so the shell must be known beforehand.
- They can only be used when using either
exec
orexecFile
/spawn
(or their respective synchronous versions) with a shell.
In Node.js you can provide environment variables to the node:child_process
module's functions using the options.env
object, for example:
import { exec } from "node:child_process";
const userInput = "&& ls";
try {
const options = {
env: { USER_INPUT: userInput },
};
// Typical Unix shell
exec(`echo 'Hello' "$USER_INPUT"`, options);
// Windows PowerShell
exec(`echo 'Hello' "$Env:USER_INPUT"`, options);
// Windows Command Prompt
exec(`echo Hello %USER_INPUT%`, options);
} catch (error) {
console.log("invalid environment, error:", error);
}
Some CLI program support the special option --
. If supported, arguments after
this option will not be interpreted as options/flags.
NOTE: Always verify that the program you're invoking supports --
.
import { exec } from "node:child_process";
import { Shescape } from "shescape";
const userInput = "foobar.txt";
// Good
let shescape = new Shescape({ flagProtection: true });
exec(`git clean -n ${shescape.quote(userInput)}`);
// Better
shescape = new Shescape({ flagProtection: false });
exec(`git clean -n -- ${shescape.quote(userInput)}`);
... without an explicit shell (or the synchronous variants execFileSync
or
spawnSync
).
These functions spawn the command directly without first spawning a shell -
provided the shell
option is left undefined. As a result, most shell injection
attacks are prevented by using these functions.
import { exec } from "node:child_process";
import { Shescape } from "shescape";
const shescape = new Shescape({ shell: false });
const userInput = "&& ls";
execFile("echo", shescape.escapeAll(["Hello", userInput, "!"]));
The use of Shescape here provides extra protection, for example around control characters.
In this section you can find things that DO NOT work to protect against shell injection.
A blocklist (sometimes called a blacklist) is an ineffective way to to protect against shell injection. This is because it is likely you will forget to block something. If you think your only option is a blocklist, use a shell escape library like Shescape instead.
Content licensed under CC BY-SA 4.0; Code snippets under the MIT license.