HIGH man in the middleexpresshmac signatures

Man In The Middle in Express with Hmac Signatures

Man In The Middle in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Using HMAC signatures in an Express API is intended to verify the integrity and origin of each request. A client computes a signature over selected parts of the request (method, path, selected headers, and body) using a shared secret, and the server recomputes and compares the signature. When this pattern is implemented incompletely over HTTP, or without strict validation, it can create or expose a Man In The Middle (MitM) condition where an on-path attacker can observe, modify, or replay requests without detection.

Three dimensions interact here: the protocol (use of HTTP instead of HTTPS), the signature scope (what is included in the signed payload), and runtime handling (how the server verifies the signature). If you serve Express over plain HTTP, an attacker on the same network can intercept and alter both the request body and the signature header, because the attacker can observe the shared secret if it is embedded in client-side code or transmitted insecurely. Even when HTTPS is used, a weak signature scope can leave parts of the request unsigned, allowing an attacker to modify unsigned headers or parameters while keeping the signature valid. Finally, if the server does not enforce strict replay protections (such as a nonce or timestamp), an intercepted, valid request can be replayed later to perform unauthorized actions, a classic MitM-derived threat despite the presence of HMAC.

Consider an Express endpoint that signs only the raw body but does not include the HTTP method, the request path, or a server-assigned nonce. An attacker can intercept the request, change the method from POST to a different permitted action, or route the request to a different endpoint while keeping the body and signature unchanged if the server does not validate the target path and method as part of the signature. Similarly, if the server accepts any timestamp within a large window without strict monotonicity checks, an attacker can capture a valid signed request and replay it within the allowed window, effectively executing a MitM replay attack.

In an OpenAPI specification context, these risks are often visible when security schemes describe an apiKey or a custom header for the signature but omit requirements for HTTPS or fail to bind the signature to the exact request target and critical headers. middleBrick scans such specs and runtime behavior, detecting missing HTTPS enforcement, incomplete signature coverage, and missing replay protections as part of its 12 security checks, including Input Validation and Authentication.

Hmac Signatures-Specific Remediation in Express — concrete code fixes

To reduce MitM risk when using HMAC signatures in Express, you must combine transport security, canonical signing inputs, and strict verification logic. Below are concrete, realistic examples you can adapt.

1. Enforce HTTPS in production

Always terminate TLS at the edge or in your hosting environment and instruct Express to treat requests as secure when behind a trusted proxy. This prevents on-path network interception.

const express = require('express');
const app = express();

// Trust the first proxy; set secure cookies and strict transport when in production
app.set('trust proxy', 1);
app.use((req, res, next) => {
  if (req.secure) {
    return next();
  }
  // In production, reject or redirect to HTTPS. This example redirects for clarity.
  return res.redirect(301, 'https://' + req.headers.host + req.url);
});

app.listen(443, () => console.log('HTTPS server running'));

2. Canonicalize the signed string

Include the HTTP method, request path, selected headers, timestamp, nonce, and body in the signature. This prevents attackers from changing method or path without invalidating the signature.

const crypto = require('crypto');
const SHARED_SECRET = process.env.SHARED_SECRET; // store securely, never ship in client code

function buildString(req) {
  const timestamp = req.headers['x-timestamp'];
  const nonce = req.headers['x-nonce'];
  const payload = [
    req.method.toUpperCase(),
    req.path.toLowerCase(),
    timestamp || '',
    nonce || '',
    req.get('content-type') || '',
    req.is('application/json') ? JSON.stringify(req.body) : ''
  ].join('\n');
  return payload;
}

function computeHmac(payload) {
  return crypto.createHmac('sha256', SHARED_SECRET).update(payload).digest('hex');
}

function verifySignature(req, res, next) {
  const expected = computeHmac(buildString(req));
  const actual = req.headers['x-signature'];
  if (!actual || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(actual))) {
    return res.status(401).json({ error: 'invalid_signature' });
  }
  // Optional: reject requests with timestamps too far from current time
  const requestTs = parseInt(req.headers['x-timestamp'], 10);
  const allowedSkew = 300; // 5 minutes
  if (Math.abs(Date.now() / 1000 - requestTs) > allowedSkew) {
    return res.status(400).json({ error: 'timestamp_expired' });
  }
  // Optional: maintain a short cache of nonces to prevent replays within the window
  next();
}

// Apply to routes that require HMAC
app.use(express.json());
app.use((req, res, next) => {
  if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') {
    return verifySignature(req, res, next);
  }
  next();
});

app.post('/api/resource', (req, res) => {
  res.json({ received: req.body });
});

3. Include critical headers and scope in verification

Sign the target path and method explicitly, and require important headers (like content-type) to be part of the signed string. Do not accept arbitrary query parameters to alter the signed input without including them in the signature.

4. Use short-lived nonces or timestamps to prevent replay

Bind each request with a one-time nonce or tightly bounded timestamp and track used nonces for a short window. This ensures that even if an attacker captures a valid request, they cannot replay it safely.

5. Rotate secrets and avoid exposing them client-side

Keep the shared secret on the server only. If clients must compute HMAC (for example in a mobile app), use per-session ephemeral keys exchanged via a secure asymmetric handshake, and never embed long-term secrets in JavaScript or mobile binaries.

Frequently Asked Questions

Can HMAC signatures alone prevent MitM if I use HTTP instead of HTTPS?
No. Without HTTPS, an on-path attacker can read and modify both the request and the signature. HMAC ensures integrity only if the transmission itself is protected; always use TLS in production.
What should I include in the signed string to minimize risk?
Include the HTTP method, request path, timestamp, nonce, content-type, and the canonicalized body. Do not omit the target path or allow the method to be swapped without invalidating the signature.