Command Injection in Express
How Command Injection Manifests in Express
Command injection in Express applications typically occurs when user input flows into Node.js child process APIs without proper sanitization. Express's role as a web framework means it often handles HTTP parameters that developers then pass to system commands.
The most common pattern involves using child_process.exec(), execSync(), or spawn() with user-controlled data. For example, an Express route that accepts a filename parameter and passes it to a shell command creates an immediate injection vector:
Express-Specific Detection
Detecting command injection in Express requires both static code analysis and dynamic testing. Static analysis tools can identify dangerous patterns like direct use of child_process.exec() with user input. Look for these Express-specific patterns:
// Dangerous pattern - user input flows directly to shell
app.get('/download', (req, res) => {
const filename = req.query.file;
exec(`cat /files/${filename}`, (err, stdout) => {
res.send(stdout);
});
});
Dynamic scanning with middleBrick specifically tests Express endpoints by sending payloads designed to trigger command execution. The scanner attempts to inject semicolons, ampersands, and other shell metacharacters into request parameters and observes whether the server responds with command execution evidence.
middleBrick's black-box approach sends payloads like:
; echo 'middleBrick_test'
& echo 'middleBrick_test'
| echo 'middleBrick_test'
$(echo 'middleBrick_test')
The scanner monitors for timing differences, error messages containing injected content, or unexpected output that indicates successful injection. For Express applications specifically, middleBrick tests both URL parameters and JSON body parameters that might flow to child processes.
middleBrick's LLM security checks also detect if Express endpoints serve as interfaces to AI models, where prompt injection could lead to command execution in the model's execution environment.
Express-Specific Remediation
Express developers should replace shell-based approaches with safer alternatives. The primary fix is using child_process.execFile() or spawn() with explicit arguments arrays instead of shell commands:
// Safe approach - no shell interpretation
app.get('/download', (req, res) => {
const filename = path.basename(req.query.file); // Prevent path traversal
const filePath = path.join('/files', filename);
// Use execFile with arguments array
execFile('cat', [filePath], (err, stdout) => {
if (err) return res.status(500).send('Error');
res.send(stdout);
});
});
Another Express-specific pattern involves using the shell-quote library to safely escape arguments when shell execution is unavoidable:
const shellQuote = require('shell-quote');
app.post('/run-command', (req, res) => {
const { command, args } = req.body;
// Validate command against whitelist
const allowedCommands = ['ls', 'pwd', 'whoami'];
if (!allowedCommands.includes(command)) {
return res.status(400).send('Invalid command');
}
// Escape arguments safely
const escapedArgs = args.map(arg => shellQuote.quote([arg]));
const fullCommand = `${command} ${escapedArgs.join(' ')}`;
exec(fullCommand, (err, stdout) => {
if (err) return res.status(500).send('Error');
res.send(stdout);
});
});
For file operations commonly handled in Express routes, use Node.js fs module instead of shell commands:
// Replace shell cat with fs.readFile
app.get('/download', (req, res) => {
const filename = path.basename(req.query.file);
const filePath = path.join('/files', filename);
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) return res.status(500).send('Error');
res.send(data);
});
});
Implement input validation using Express middleware to sanitize parameters before they reach command execution logic:
const sanitizeInput = (req, res, next) => {
const dangerousChars = /[;|&$`()<>"'\s]/;
for (const [key, value] of Object.entries(req.query)) {
if (dangerousChars.test(value)) {
return res.status(400).send('Invalid input');
}
}
next();
};
app.get('/safe-download', sanitizeInput, (req, res) => {
// Safe to use req.query.file here
const filename = path.basename(req.query.file);
fs.readFile(path.join('/files', filename), (err, data) => {
res.send(data || '');
});
});
Related CWEs: inputValidation
CWE ID Name Severity CWE-20 Improper Input Validation HIGH CWE-22 Path Traversal HIGH CWE-74 Injection CRITICAL CWE-77 Command Injection CRITICAL CWE-78 OS Command Injection CRITICAL CWE-79 Cross-site Scripting (XSS) HIGH CWE-89 SQL Injection CRITICAL CWE-90 LDAP Injection HIGH CWE-91 XML Injection HIGH CWE-94 Code Injection CRITICAL
Frequently Asked Questions
How does middleBrick detect command injection in Express applications?
middleBrick performs black-box scanning by sending payloads containing shell metacharacters to Express endpoints. It tests URL parameters, JSON body parameters, and headers for injection vulnerabilities. The scanner looks for timing differences, error messages containing injected content, or unexpected output that indicates successful command execution. For Express specifically, it tests both GET and POST endpoints that might pass user input to child processes.Can command injection in Express lead to remote code execution?
Yes, command injection in Express can lead to full remote code execution if the injected commands are executed with sufficient privileges. An attacker could upload reverse shells, modify system files, or execute arbitrary scripts. The severity depends on the Node.js process permissions and whether the application runs as a privileged user. Using exec() with user input is particularly dangerous because it provides full shell access.