HIGH command injectionexpresshmac signatures

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 execFile or spawn with 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.timingSafeEqual for 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 IDNameSeverity
CWE-20Improper Input Validation HIGH
CWE-22Path Traversal HIGH
CWE-74Injection CRITICAL
CWE-77Command Injection CRITICAL
CWE-78OS Command Injection CRITICAL
CWE-79Cross-site Scripting (XSS) HIGH
CWE-89SQL Injection CRITICAL
CWE-90LDAP Injection HIGH
CWE-91XML Injection HIGH
CWE-94Code Injection CRITICAL

Frequently Asked Questions

Does using an HMAC prevent command injection in Express APIs?
No. An HMAC can ensure data integrity, but it does not validate the safety of inputs. If untrusted values are passed to shell commands without allowlisting and proper escaping, injection can still occur despite a valid HMAC.
What are safer alternatives to invoking shell commands in Express when handling user-influenced data?
Prefer direct argument passing with execFile or spawn, use strict allowlist validation on all inputs, and avoid building shell command strings. For complex operations, consider isolated workers or APIs that do not require shell invocation.