Description
In Node.js, using {shell: true} or {shell: '/bin/sh'} with child_process.spawn or spawnSync runs an external command through a shell. Special characters in user input may then be interpreted as commands, allowing an attacker to inject unintended operations.
Potential impact
- Malicious commands that access, delete, or expose server files
- Exposure, modification, or destruction of sensitive data
- Service disruption through resource exhaustion or process termination
Remediation
- Use a fixed executable and pass arguments as an array with
{shell: false}. - If a shell is required, restrict commands and arguments to a trusted allow-list.
- Validate external input before using it in commands or arguments. Do not rely on quoting alone.
Examples
Before
javascript
const { spawn } = require("child_process");
const app = require("express")();
app.get("/list", (req, res) => {
// Request input becomes an argument interpreted by the shell.
const userInput = req.query.path;
const p = spawn("ls", ["-lh", userInput], { shell: true });
});
After
javascript
const { spawn } = require("child_process");
const app = require("express")();
app.get("/list", (req, res) => {
const userInput = req.query.path;
if (typeof userInput !== "string" || !/^[a-zA-Z0-9_-]+$/.test(userInput)) {
return res.sendStatus(400);
}
// Pass validated arguments to a fixed executable without a shell.
const p = spawn("ls", ["-lh", "--", userInput], { shell: false });
});
Explanation:
- Before: With
shell: true, special characters in user input may be interpreted by the shell as commands. - After:
shell: false(the default) avoids shell interpretation, and the input format is restricted.--stopslsfrom treating the input as an option. The executable location and access to the directories being listed still need separate controls.