Google PlayGoogle Play
Temp Mail Logo

Temp Mail safeguards your privacy while keeping your inbox free from spam.

← Back to Blog
Privacy

How to Verify a Webhook Signature (HMAC-SHA256) Correctly

Best-TempMail Team2026-10-01
How to Verify a Webhook Signature (HMAC-SHA256) Correctly

How to Verify a Webhook Signature (HMAC-SHA256) Correctly

To verify webhook signature hmac headers, you must recompute the cryptographic hash of the incoming request's raw body and compare it to the signature provided in the HTTP header. This process ensures the data originated from a trusted source and remained unaltered during transit.

The direct answer is a three-step technical requirement:

  1. Capture the Raw Body: You must access the unparsed, original byte stream of the request.
  2. Compute the HMAC: Use the SHA-256 algorithm, your shared secret key, and that raw byte stream to generate a local signature.
  3. Compare Safely: Use a constant-time comparison function to match your local signature against the header's signature to prevent timing attacks.

Failure to implement this correctly leaves your application vulnerable to request forgery, allowing attackers to inject fraudulent data into your database or trigger unauthorized internal actions.


Why Traditional Authentication Fails for Webhooks

Standard web security patterns often fall short when applied to asynchronous webhook architectures. Because webhooks are pushed from a third-party server to your public endpoint, you cannot rely on traditional user-session-based authentication.

The Failure of IP Allowlists

Many developers attempt to secure endpoints by restricting traffic to specific IP ranges. This is fragile. SaaS providers frequently rotate their outbound IP addresses to maintain deliverability and scale. Relying on hardcoded IPs creates a maintenance burden and offers no protection against IP spoofing within internal or misconfigured networks.

The Risk of Secret Query Parameters

Appending a "token" to the webhook URL (e.g., /webhook?token=secret_value) is a common but dangerous shortcut. URLs are logged in plain text by reverse proxies, load balancers, and browser histories. If an attacker gains access to your logs, they gain full access to your webhook endpoint.

The Fragility of Re-serialization

A common mistake is attempting to verify a signature using a JSON object that has already been parsed by a web framework. When a framework like Express or Flask converts a JSON string into an object, it discards the original formatting. Re-serializing that object back into a string often changes key order, whitespace, or character escaping. Because HMAC-SHA256 is sensitive to the exact byte sequence, even a single added space will cause the verification to fail.


The Mechanics of HMAC-SHA256 Verification

HMAC (Hash-based Message Authentication Code) provides a way to verify both the integrity and the authenticity of a message. It uses a cryptographic hash function—in this case, SHA-256—combined with a secret key known only to the sender and the receiver.

The Sender's Responsibility

When a service provider sends a webhook, they perform the following:

  1. Generate the JSON payload.
  2. Apply the HMAC-SHA256 algorithm to the raw JSON string using a shared secret.
  3. Encode the resulting hash (usually as a hexadecimal string).
  4. Include this hash in a header, such as X-Hub-Signature-256.

The Receiver's Responsibility

Your server must mirror this logic. You take the incoming raw bytes, apply the same secret and algorithm, and check if your result matches theirs. If the signatures match, you have mathematical proof that the sender possesses the secret and the message has not been tampered with.


How to Verify Webhook Signature HMAC in Node.js

In Node.js, the primary challenge is preventing the default body-parser from mutating the request before you can verify it. You must configure your middleware to provide the raw buffer.

The following implementation uses the native crypto module and Express:

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

const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Use express.raw to keep the body as a Buffer
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature-sha256'];

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

  // 1. Recompute the HMAC using the raw Buffer
  const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
  hmac.update(req.body);
  const digest = hmac.digest('hex');

  // 2. Prepare buffers for constant-time comparison
  const trusted = Buffer.from(digest, 'ascii');
  const received = Buffer.from(signature.replace('sha256=', ''), 'ascii');

  // 3. Prevent timing attacks with timingSafeEqual
  if (trusted.length !== received.length || !crypto.timingSafeEqual(trusted, received)) {
    return res.status(403).send('Invalid signature');
  }

  // 4. Parse the JSON only after verification
  try {
    const payload = JSON.parse(req.body.toString());
    console.log('Verified Payload:', payload);
    res.status(200).send('OK');
  } catch (err) {
    res.status(400).send('Invalid JSON');
  }
});

app.listen(3000);

Understanding timingSafeEqual

In the code above, crypto.timingSafeEqual is critical. Standard string comparisons (==) return as soon as they find a character mismatch. An attacker can measure the time it takes for your server to respond to different signatures to "guess" the correct hash byte-by-byte. A constant-time function ensures the comparison takes the same amount of time regardless of where a mismatch occurs.


How to Verify Webhook Signature HMAC in Python

Python’s Flask and Django frameworks provide access to the raw request data via request.get_data() or request.body. The hmac library includes a built-in comparison function designed to thwart timing attacks.

import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET').encode('utf-8')

@app.route('/webhook', methods=['POST'])
def webhook_handler():
    # Extract the signature from the header
    signature = request.headers.get('X-Signature-SHA256')
    if not signature:
        abort(401)

    # Get the raw unparsed bytes
    raw_data = request.get_data()

    # Compute the local HMAC-SHA256
    local_hash = hmac.new(
        WEBHOOK_SECRET,
        raw_data,
        hashlib.sha256
    ).hexdigest()

    # Clean the header signature if it contains a prefix
    received_hash = signature.replace('sha256=', '')

    # Use compare_digest for constant-time validation
    if not hmac.compare_digest(local_hash, received_hash):
        abort(403)

    # Proceed with processing the verified data
    data = request.get_json()
    return 'Success', 200

if __name__ == '__main__':
    app.run(port=3000)

Critical Security Pitfalls to Avoid

1. The Replay Attack

Even with HMAC verification, an attacker can capture a valid request and "replay" it to your server multiple times. The signature will still be valid because the payload and secret haven't changed. To prevent this, many providers include a timestamp header. You should verify that the timestamp is within a reasonable window (e.g., the last 5 minutes) before checking the signature.

2. Character Encoding Mismatches

Always ensure you are treating the raw body as a byte array or a UTF-8 encoded string. If your server defaults to a different encoding (like Latin-1), the HMAC calculation will result in a different hash for any payload containing special characters or emojis, leading to false negatives in your verification logic.

3. Logging Sensitive Data

Never log the WEBHOOK_SECRET or the full signature headers in your production logs. If your logging system is compromised, the attacker can use these secrets to forge requests that bypass all your security checks.


Testing Webhooks in CI/CD Pipelines

Testing webhook logic in a local environment is difficult because external services cannot reach localhost. To build reliable integration tests, you need a way to simulate incoming signed requests or use a service that provides programmatic endpoints.

When automating tests for transactional systems, developers often use Best-TempMail to generate temporary, programmatic email addresses. On professional tiers, this service can forward incoming emails to your webhook URL as signed HMAC-SHA256 payloads. This allows your CI/CD pipeline to verify that your application correctly handles, verifies, and processes incoming data without manual intervention.

For more complex testing scenarios, such as choosing between real-time notifications and scheduled checks, see our guide on webhooks vs polling for email testing. If your webhooks are part of a security workflow, you may also need to consider automating OTP verification in end-to-end tests.


Comparing Webhook Security Models

Not every integration requires HMAC. Depending on your infrastructure, other methods might be more appropriate.

Option 1: HMAC-SHA256 (The Standard)

Security Level: High

Implementation: Requires raw body access and shared secret management.

Best For: Most SaaS integrations and public-facing APIs.

Option 2: Mutual TLS (mTLS)

Security Level: Very High

Implementation: Requires client certificates and infrastructure-level configuration.

Best For: High-compliance financial or healthcare environments where the transport layer itself must be authenticated.

Option 3: API Polling

Security Level: High (Pull vs. Push)

Implementation: Your server requests data from the provider on a schedule.

Best For: Systems behind strict firewalls that cannot accept inbound traffic. You can use our email tools to inspect these interactions manually during development.


Operational Security and Secret Management

The security of your HMAC verification is only as strong as your secret management. If your WEBHOOK_SECRET is committed to a git repository, it is compromised.

  • Environment Variables: Store secrets in encrypted environment variables or dedicated secret managers like AWS Secrets Manager or HashiCorp Vault.
  • Secret Rotation: Periodically rotate your webhook secrets. To do this without downtime, your code should support "graceful rotation"—checking the incoming signature against both the new secret and the old secret for a 24-hour transition period.
  • Rate Limiting: Cryptographic operations are CPU-intensive. An attacker could flood your endpoint with large payloads to exhaust your CPU. Implement rate limiting and maximum payload size checks before you run the HMAC calculation.

FAQ

What is the difference between SHA-256 and HMAC-SHA256?

SHA-256 is a one-way hash function that produces a fixed-size digest of any input. HMAC-SHA256 is a specific construction that mixes a secret key with the input before hashing it. This prevents "length extension attacks" where an attacker could potentially append data to a message and calculate a new valid SHA-256 hash without knowing the secret.

Why is my signature verification failing in production but working locally?

This is almost always due to a proxy or load balancer. Services like Cloudflare, Nginx, or AWS ALB can be configured to modify headers or "clean up" the body of a request (e.g., removing trailing newlines). If the body is modified by even one byte between the sender and your verification code, the HMAC will not match. Ensure your ingress controllers are configured to pass the request body through as an immutable stream.

Can I use a standard string comparison for the signatures?

No. You must use a constant-time comparison function. Standard string comparison operators return false as soon as a mismatch is found, which creates a timing side-channel. An attacker can use this to determine how many characters of their forged signature are correct by measuring the nanoseconds it takes for your server to reject the request.

Should I verify the signature if I am using HTTPS?

Yes. HTTPS only encrypts the data in transit, protecting it from eavesdroppers. It does not prove that the sender is who they claim to be. HMAC provides the "Authentication" part of the security puzzle, while HTTPS provides the "Privacy" part. You need both for a secure production environment.

How do I handle multiple webhook providers with different secrets?

You should map your webhook endpoints to specific secrets. For example, requests to /webhooks/stripe should use your Stripe secret, while /webhooks/github uses your GitHub secret. Never attempt to "guess" which secret to use by iterating through a list of all known secrets for every request, as this increases your vulnerability to DoS attacks.

Free · Instant · Anonymous

Your temp mail is ready right now

No signup, no password. A disposable inbox waiting the moment you open the page.

Get My Free Temp Mail →