Command Injection in Express with Hmac Signatures
Command Injection in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Command injection occurs when an attacker can cause an application to execute arbitrary system commands. In Express, this risk can arise when request data influences shell commands, and HMAC signatures intended to verify integrity are mishandled in a way that allows untrusted input to reach the command layer.
Consider an endpoint that uses an HMAC to validate a webhook or callback. If the server extracts a parameter from the payload, includes it in a shell command (for example to pass a filename or a user identifier), and does not sufficiently validate or sanitize that parameter, the HMAC may still verify successfully while the parameter is under attacker control. An attacker can craft a signature on arbitrary input, and if the server trusts that signature without additional checks, the injected payload can be executed with the privileges of the application process.
A concrete pattern involves computing an HMAC over concatenated fields (e.g., timestamp and resource ID) and then using one of those fields in a shell command. If the server does not re-validate the logical constraints of those fields independently of the cryptographic signature, an attacker can vary the payload while keeping the signature valid, leading to command injection. For example, an attacker might inject shell metacharacters or use command chaining operators to run additional commands. This combines a broken validation boundary with a trusted-but-malicious input path, which is common when signatures are treated as authorization rather than integrity checks.
Middleware that parses JSON bodies and computes HMACs can inadvertently pass raw values to child processes or to libraries that invoke shell commands. Without strict allowlisting, type checks, and separation between integrity verification and execution logic, the HMAC does not prevent injection—it only confirms that the attacker supplied a consistent set of inputs.
Real-world analogs include scenarios where an API processes a file identifier or a webhook event ID that is reflected in system commands. If the identifier is not constrained to a safe pattern (e.g., alphanumeric with a fixed length), an attacker can leverage shell operators such as &&, ||, or backticks to alter command flow. OWASP API Top 10’s Broken Object Level Authorization and Injection categories intersect here, and the presence of an HMAC does not mitigate injection if input validation and process invocation are not designed defensively.
Hmac Signatures-Specific Remediation in Express — concrete code fixes
To remediate command injection when using HMAC signatures in Express, enforce strict input validation, avoid shell usage, and ensure cryptographic verification is separate from execution logic. Below are concrete, safe patterns.
Example 1: Validate before using in a command
Use an allowlist regex for expected values and avoid shell interpolation entirely. If you must invoke a subprocess, prefer direct argument passing instead of a shell string.
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.WEBHOOK_SECRET;
function verifyHmac(payload, receivedSig) {
const hmac = crypto.createHmac('sha256', SHARED_SECRET);
hmac.update(JSON.stringify(payload));
return hmac.digest('hex') === receivedSig;
}
app.post('/webhook', (req, res) => {
const sig = req.get('x-hub-signature-256');
if (!sig || !verifyHmac(req.body, sig)) {
return res.status(401).send('Invalid signature');
}
const { fileId } = req.body;
// Allowlist validation: only safe characters and length
if (!/^[a-zA-Z0-9_-]{1,32}$/.test(fileId)) {
return res.status(400).send('Invalid file identifier');
}
// Use direct arguments instead of shell; example uses child_process safely
const { execFile } = require('child_process');
execFile('/usr/bin/process-file', [fileId], (err, stdout, stderr) => {
if (err) {
console.error(err);
return res.status(500).send('Processing failed');
}
res.send(stdout);
});
});
app.listen(3000, () => console.log('Server running on port 3000'));
Example 2: Reject unexpected fields and use constant-time comparison
Ensure the set of signed fields is fixed and reject extra parameters to prevent parameter pollution attacks that could alter behavior without breaking the signature.
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.json());
const SHARED_SECRET = process.env.WEBHOOK_SECRET;
function verifyHmacStrict(payload, receivedSig) {
const allowedKeys = ['event', 'timestamp', 'resourceId'];
const filtered = {};
for (const k of allowedKeys) {
if (payload[k] === undefined) return false;
filtered[k] = payload[k];
}
// Ensure no extra keys
if (Object.keys(payload).length !== allowedKeys.length) return false;
const hmac = crypto.createHmac('sha256', SHARED_SECRET);
hmac.update(JSON.stringify(filtered));
// Constant-time compare
return crypto.timingSafeEqual(Buffer.from(hmac.digest('hex')), Buffer.from(receivedSig));
}
app.post('/event', (req, res) => {
const sig = req.get('x-signature');
if (!sig || !verifyHmacStrict(req.body, sig)) {
return res.status(401).send('Invalid signature');
}
const { resourceId } = req.body;
// Further logical validation
if (typeof resourceId !== 'string' || !/^[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}$/.test(resourceId)) {
return res.status(400).send('Invalid resourceId');
}
// Safe usage: pass as argument, never concatenate into shell strings
const { spawn } = require('child_process');
const proc = spawn('/usr/bin/analyze', ['--id', resourceId]);
let data = '';
proc.stdout.on('data', (chunk) => data += chunk);
proc.on('close', (code) => {
if (code !== 0) return res.status(502).send('Analysis failed');
res.send(data);
});
});
app.listen(3000, () => console.log('Server running on port 3000'));
Key practices
- Never build shell commands by concatenating user-influenced strings; use
execFileorspawnwith an array of arguments. - Apply allowlist validation on all inputs that reach the command layer, independent of HMAC verification.
- Treat HMACs as integrity checks, not authorization; re-validate business rules and constraints separately.
- Use
crypto.timingSafeEqualfor signature comparisons to avoid timing attacks. - If you need to integrate with existing tooling that requires shell commands, strictly sanitize and escape, but prefer direct invocation paths.
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 |