HIGH webhook abuseexpresshmac signatures

Webhook Abuse in Express with Hmac Signatures

Webhook Abuse in Express with Hmac Signatures — how this specific combination creates or exposes the vulnerability

Webhook abuse in Express when using HMAC signatures typically arises from incomplete verification or trusting untrusted inputs. A common pattern is for a provider to sign a JSON payload with a shared secret and send it to an Express route via a header such as x-hub-signature-256. If the server does not enforce strict signature validation, an attacker can forge requests by guessing or leaking the secret, or by exploiting weak comparison logic.

Express does not provide built-in HMAC verification; developers implement it manually. A vulnerable implementation might compute HMAC on the raw body buffer but then compare it using a non-constant-time function, or fail to ensure the body is not modified between receipt and verification. If the body is parsed before computing the HMAC (for example, using express.json() middleware), the serialized form may differ from what the provider used, causing signature mismatch or leading developers to disable verification to work around mismatches.

Another abuse vector is replay attacks: without additional protections such as timestamps or nonces, a valid HMAC-signed payload can be replayed to trigger unintended actions. Also, if the shared secret is stored or transmitted insecurely, or if the signature header is not validated for presence and format, an attacker may bypass verification entirely. These issues are exacerbated when developers assume that HMAC alone is sufficient without additional checks like idempotency keys or strict origin checks.

Consider an endpoint that processes payment events. If the signature is computed over the parsed JSON object rather than the raw body, slight formatting differences (whitespace, key ordering) can cause verification to fail, tempting developers to skip verification or to accept unsigned requests in production. This deviates from best practices where the raw body buffer should be preserved and verified before any parsing.

To detect such misconfigurations, scans include checks for missing or weak HMAC verification, body parsing before signature validation, and lack of replay resistance. These checks highlight the need to treat the HMAC as a mandatory integrity control and to design the route so that verification is strict, constant-time, and performed before any business logic is executed.

Hmac Signatures-Specific Remediation in Express — concrete code fixes

To remediate webhook abuse with HMAC signatures in Express, ensure you verify the signature over the raw body using a constant-time comparison, and avoid parsing the body before verification. Below are concrete, working examples.

1. Preserve raw body and verify HMAC (Express 4/5)

Use a raw body buffer for HMAC computation and compare signatures safely.

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

// Secret should be stored securely (e.g., environment variable)
const SHARED_SECRET = process.env.WEBHOOK_SECRET;
if (!SHARED_SECRET) {
  throw new Error('WEBHOOK_SECRET must be set');
}

// Middleware to capture raw body for HMAC verification
app.use(express.json({ type: 'application/json', verify: (req, res, buf) => {
  req.rawBody = buf; // store raw body for signature verification
}}));

app.post('/webhook', (req, res) => {
  const signatureHeader = req.get('x-hub-signature-256');
  if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
    return res.status(400).send('Missing signature');
  }

  const receivedHash = signatureHeader.split('=')[1];
  const hmac = crypto.createHmac('sha256', SHARED_SECRET);
  const computedHash = hmac.update(req.rawBody).digest('hex');

  // Use timing-safe comparison to avoid timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(receivedHash, 'hex'),
    Buffer.from(computedHash, 'hex')
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // At this point, the payload is authentic; parse and process
  const payload = req.body;
  // TODO: idempotency and replay protection (e.g., check event ID/timestamp)
  res.status(200).send('OK');
});

module.exports = app;

2. Enforce strict header presence and replay resistance (optional enhancements)

Add replay protection by validating timestamps and unique event identifiers. This complements HMAC and reduces abuse windows.

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

const SHARED_SECRET = process.env.WEBHOOK_SECRET;
const seenEvents = new Set(); // in production, use a fast TTL cache

app.use(express.json({ type: 'application/json', verify: (req, res, buf) => {
  req.rawBody = buf;
}}));

app.post('/webhook', (req, res) => {
  const signatureHeader = req.get('x-hub-signature-256');
  if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
    return res.status(400).send('Missing signature');
  }

  const receivedHash = signatureHeader.split('=')[1];
  const hmac = crypto.createHmac('sha256', SHARED_SECRET);
  const computedHash = hmac.update(req.rawBody).digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(receivedHash, 'hex'),
    Buffer.from(computedHash, 'hex')
  )) {
    return res.status(401).send('Invalid signature');
  }

  const payload = req.body;
  const eventId = payload.id || req.get('x-event-id');
  const timestamp = payload.timestamp;

  // Basic replay protection: reject duplicates and stale events
  if (!eventId) {
    return res.status(400).send('Missing event ID');
  }
  if (seenEvents.has(eventId)) {
    return res.status(409).send('Duplicate event');
  }
  const now = Date.now();
  const eventAgeMs = now - new Date(timestamp).getTime();
  const maxAgeMs = 5 * 60 * 1000; // 5 minutes
  if (eventAgeMs > maxAgeMs) {
    return res.status(400).send('Stale event');
  }

  seenEvents.add(eventId);
  // Process the verified event
  res.status(200).send('OK');
});

module.exports = app;

3. Security and compliance mappings

These implementations align with OWASP API Security Top 10 controls and map to compliance frameworks. They address:

  • Broken Object Level Authorization (BOLA): Ensure the webhook action is authorized for the intended scope; do not rely on the event payload alone for authorization decisions without additional context checks.
  • Improper Inventory Management: Track and rotate shared secrets; do not embed secrets in source code.
  • Unsafe Consumption: Validate and sanitize parsed payload fields before use, even after HMAC verification.

For production, store WEBHOOK_SECRET in a secure vault, rotate keys periodically, and use short TTL caches for replay protection. The GitHub Action can enforce a minimum score and fail builds if HMAC-related findings are present, while the CLI allows local verification with middlebrick scan <url>. The MCP Server enables scanning from AI coding assistants when developing webhook handlers.

Frequently Asked Questions

Why should I verify the raw body instead of the parsed JSON when using HMAC signatures in Express?
Parsing JSON with express.json() may reorder keys or change whitespace, causing the computed HMAC to differ from the provider's signature. To verify correctly, keep the raw body buffer and compute HMAC over it before any parsing, then compare using a constant-time function like crypto.timingSafeEqual.
Does HMAC verification prevent replay attacks on webhooks?
HMAC ensures integrity and authenticity of a single request, but does not prevent replay attacks. Add replay resistance by including an event ID and timestamp in the payload, maintaining a short-lived allowlist of seen event IDs, and rejecting stale timestamps.