Webhook Security Vulnerabilities Guide
Webhook endpoints are publicly accessible URLs that accept HTTP requests from external services — making them a potential attack surface if not properly secured. The primary security risks are man-in-the-middle attacks (intercepting unencrypted payloads), forged requests (attackers sending fake webhooks), replay attacks (re-sending captured requests), PII exposure (sensitive data in transit and at rest), and insufficient access control (unauthorized endpoint configuration). Mitigating these risks requires HMAC signature verification, HTTPS enforcement, timestamp validation, idempotent processing, and proper access controls.
For a concise security checklist, see our webhook security checklist. Below we cover each vulnerability in depth.
There are 3 main vectors of attacks that you need to watch out for and protect yourself against, plus additional security considerations around access control, audit trails, and data handling.
Man-in-the-middle Attack
A man-in-the-middle attack is a vulnerability where a third party obtains access to your webhook data by capturing and reading the request. It's essential that you only work with HTTPS URLs (using SSL) when working with sensitive data. Some providers such as Shopify will enforce this restriction, but many platforms will let you input unencrypted URLs. By using HTTPS, the content of the request is encrypted and can't be read or used by anyone that intercepts the requests.
While technically not a man-in-the-middle attack, it's also possible to provide a URL that you do not own. The burden is on you to make sure the URL that you provide as your Webhook URL points to your own server that this server is also secure. Some platforms like Okta will perform a one-time verification to validate that you are indeed the owner of the URL and will not send any webhooks until that verification is performed. Otherwise, audit your server logs and configured webhook URLs to ensure they are pointing to the correct address.
Forged requests
A forged request is a request made to your webhook endpoint that acts as if it was sent from the original source (ex: Shopify), but contains forged data. It's critical to ensure that bad actors are not causing side effects in your systems by sending forged requests to your webhook endpoints. There are multiple strategies to verify that the request isn't coming from an impostor, signature verification being the most popular and secure approach.
Signature verification
Platforms that sign their webhooks can be verified by comparing a value found in the headers (ex: Shopify includes an X-Shopify-Hmac-Sha256 header) with a computed HMAC made up of a secret key and the content of your webhook payload. In generating a Hash-based message authentication code (HMAC), you are required to use your payload content along with the secret key that the platform provides you when you create a webhook subscription. If the header and your computed signature match, then you can be certain the message was originally sent from the expected provider.
This works because only you and the platform have access to that secret key and can compute an identical hash. By including the body of the payload within the hash, you can also guarantee that it has not been tempered. In event that this secret is leaked, you should replace it as soon as possible.
Different providers will use different hashing algorithms (SHA256, SHA128, MD5, etc.), and your implementation should use the same algorithm. Many platforms will also provide SDKs that include the verification logic to spare you most of the trouble.
Example of adding webhook verification for Shopify in Javascript
To get started with adding webhook verification to the server, run the following command:
$ npm install raw-body
The raw-body module we just installed would help us parse our incoming request body in a way that can be used to generate the hash. The default body-parser module that comes with Express could also be used in place of the raw-body package.
Next, Import the raw-body & crypto packages into the project by adding the following lines of code:
const getRawBody = require("raw-body");
const crypto = require("crypto");
The crypto module would be used to generate the hash from our request body and the secret key provided to us by Shopify.
Replace the app.post request handler created above with this
app.post("/webhook", async (req, res) => {
//Extract X-Shopify-Hmac-Sha256 Header from the request
const hmacHeader = req.get("X-Shopify-Hmac-Sha256");
//Parse the request Body
const body = await getRawBody(req);
//Create a hash based on the parsed body
const hash = crypto
.createHmac("sha256", secret)
.update(body, "utf8", "hex")
.digest("base64");
// Compare the created hash with the value of the X-Shopify-Hmac-Sha256 Header
if (hash === hmacHeader) {
console.log("Notification Requested from Shopify received");
res.sendStatus(200);
} else {
console.log("There is something wrong with this webhook");
res.sendStatus(403);
}
});
In the code above, we extract the X-Shopify-Hmac-SHA256 HTTP header from the request, create a hash based on the Hmac-SHA256 algorithm from the request body then compare both hashes.
Important: Use constant-time comparison. The example above uses === for simplicity, but in production you should use a constant-time comparison function like crypto.timingSafeEqual() to prevent timing attacks where an attacker can deduce the correct signature byte-by-byte based on response times:
const isValid = crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(hmacHeader)
);
Common HMAC verification mistakes
- Using string comparison instead of constant-time comparison. Standard
===leaks timing information that attackers can exploit. - Parsing the body before verification. If you use Express's
json()middleware before verification, the body is parsed and re-serialized — which may change formatting and break the signature. Always verify against the raw body. - Using the wrong encoding. Some providers send Base64-encoded signatures, others send hex. Ensure your hash output encoding matches the provider's format.
- Storing the secret insecurely. Never hardcode webhook secrets in source code. Use environment variables or a secrets manager.
Lastly, go ahead and create a constant called secret which would hold the value of the secret Shopify returned to you when created a new webhook connection. You would want to store that as an environmental variable to ensure it is safe.
Secrets
Alternatively, you might find platforms with the option to add custom headers to the webhook requests, or a basic authentication header. In that case, you should generate a secret key on your own that you would set as a header and would verify on your end. This is similar to how most API authentication works and is considered secured when used over HTTPS. However, absolutely do not rely on this strategy when using standard HTTP, because any man-in-the-middle could steal your secret and forge authenticated requests
IP whitelisting
If the platform that's publishing webhooks provides a list of IPs that they send requests from, you can set up the receiving endpoint to only accept requests from those IPs. This is generally straightforward, but it depends on the platform that you're hosting on. While IP whitelisting is effective, it can also get cumbersome because you have to maintain the IP whitelist. If the provider adds an IP address, and you fail to update your list, you run the risk of potentially refusing payloads. For that reason, it should be used as a complement to other strategies listed above. Whitelisting IPs is not a requirement to protect yourself against forged requests.
Replay attacks
Even with the previous two vulnerabilities addressed, you are still susceptible to replay attacks. A bad actor could, in theory, intercept an encrypted request with signature verification and simply replay or make identical requests. While he wouldn't be able to see any of the data, nor alter it, he could still cause unintended side-effects such as creating multiple orders after a "Payment Captured" webhooks. This limits the possible consequences of this kind of attack, but nonetheless there are practical solutions to protect yourself against it.
Timestamped signatures
Some webhook providers, like Stripe, will include a timestamp within the signature. The timestamp used at the time of signing will be contained within the header and can be checked against a time window that you determine. Stripe SDK has a default tolerance of 5 minutes. That same timestamp must be appended to the signature before it's hashed.
To implement timestamp validation:
- Extract the timestamp from the webhook header
- Compare it against the current server time
- Reject requests where the difference exceeds your tolerance window (typically 5 minutes)
- Ensure your server clock is synchronized via NTP to avoid false rejections
Note: timestamp validation can conflict with webhook retries. If a provider retries a webhook after your tolerance window has passed, the timestamp check will reject the retry. Consider this when setting your tolerance — or rely on idempotency as your primary replay protection.
Nonce checking
Some providers include a unique nonce (one-time identifier) with each webhook delivery. By tracking which nonces you've already processed, you can reject any request that reuses a previously seen nonce. This is more robust than timestamp validation alone, as it catches replays even within the tolerance window. However, it requires maintaining a store of processed nonces — similar to idempotency key tracking.
Idempotency
Idempotency is a core concept when working with webhooks: if you have already ensured that your endpoints are idempotent, you won't be impacted by a replay attack. By making your endpoint idempotent you ensure that any webhooks are only processed once, even if received multiple times. That's an important case to handle regardless of replay attacks, since most (if not all) providers operate on an at least once delivery strategy. Timestamped signatures are unnecessary if idempotency is correctly implemented - idempotency is the only solution to protect yourself against attacks if your provider does not support timestamped signatures (and most don't).
RBAC and access control
Webhook security isn't just about verifying incoming requests — it also includes controlling who in your organization can configure webhook endpoints and view webhook payloads.
- Restrict endpoint configuration. Only authorized team members should be able to create, modify, or delete webhook endpoints. A misconfigured endpoint (pointing to an attacker's URL) would forward all your webhook data to a malicious server.
- Control payload visibility. Webhook payloads may contain sensitive customer data. Limit who can view raw payloads in your monitoring tools, and use role-based access to restrict access to webhook event history.
- Audit configuration changes. Log all changes to webhook configuration — who created or modified an endpoint, when, and what changed. This helps with incident investigation and compliance.
With Hookdeck, team access is controlled through organization roles, and all configuration changes are tracked in the dashboard.
Audit trails
For compliance and incident response, maintain a complete audit trail of webhook activity:
- Log every incoming webhook with timestamp, source, signature verification result, and processing outcome. This is essential for forensic analysis after a security incident.
- Track delivery attempts including all retry attempts, response codes, and timing. This helps distinguish legitimate retries from replay attacks.
- Retain logs for your compliance window. Financial services, healthcare, and other regulated industries may require webhook audit logs retained for specific periods.
Hookdeck automatically maintains a complete audit trail of all webhook events, delivery attempts, and their outcomes — searchable and filterable in the dashboard or via the API.
PII handling in webhook payloads
Webhook payloads frequently contain personally identifiable information (PII) — customer emails, phone numbers, payment details, addresses. Handling this data securely is both a security and compliance concern.
Data in transit:
- Always use HTTPS endpoints — never accept webhooks over unencrypted HTTP when payloads contain PII
- Consider whether you need the full data in the payload. Some providers allow you to receive lightweight notifications (just an event type and resource ID) and fetch the full data via their API — reducing PII exposure in transit
Data at rest:
- Be mindful of where webhook payloads are stored — your logging system, monitoring tools, and error tracking services may all retain copies
- Implement log redaction for sensitive fields (credit card numbers, SSNs, etc.)
- Apply appropriate retention policies to webhook event data
Compliance considerations:
- GDPR, HIPAA, PCI-DSS, and other regulations may govern how you handle webhook data containing PII
- Ensure your webhook processing pipeline meets the same compliance standards as the rest of your data handling
- Document your webhook data flow as part of your data processing records
For more on securing your entire webhook pipeline, see the webhook security checklist. For ensuring duplicate deliveries don't cause security-relevant side effects, see implementing webhook idempotency.
Conclusion
Webhooks, when used correctly, are very secure. As with most things, the burden often falls on you to make sure you are properly verifying signatures, using HTTPS, implementing idempotency, and managing access controls. Hookdeck handles many of these concerns at the infrastructure level — including signature verification, encrypted delivery, team access controls, and complete audit trails — so you can focus on your application logic.
FAQs
What are the main security risks with webhooks?
The main risks are man-in-the-middle attacks (intercepting unencrypted payloads), forged requests (attackers sending fake webhooks to your endpoint), replay attacks (re-sending captured requests to trigger duplicate processing), PII exposure (sensitive data in payloads), and insufficient access control (unauthorized users configuring webhook endpoints).
How do I verify webhook signatures?
Use HMAC signature verification. The provider includes a signature in the request headers computed from the payload and a shared secret. You recompute the signature using the same algorithm and secret, then compare using a constant-time comparison function to prevent timing attacks. If they match, the request is authentic and untampered.
What is a webhook replay attack?
A replay attack occurs when an attacker captures a valid, signed webhook request and re-sends it to trigger duplicate processing. Prevent this with timestamp validation (reject requests older than 5 minutes) and idempotent processing (track processed webhook IDs to detect and skip duplicates).
How do I protect sensitive data in webhook payloads?
Always use HTTPS endpoints, minimize the sensitive data included in payloads (send IDs instead of full records where possible), encrypt sensitive fields at the application level if needed, and implement access controls on who can view webhook payloads in your monitoring tools. For compliance-sensitive data, consider logging policies that redact PII.
Should webhook endpoints require authentication?
Webhook endpoints should always verify the authenticity of incoming requests, typically through HMAC signature verification. IP whitelisting can add an additional layer of security but shouldn't be the sole authentication mechanism. Never rely on security through obscurity (hidden URLs) as your only protection.
How does HMAC verification work for webhooks?
HMAC (Hash-based Message Authentication Code) verification works by computing a hash of the webhook payload using a shared secret key and a hashing algorithm (typically SHA-256). The provider computes this hash when sending and includes it in a header. You compute the same hash on receipt and compare. A match proves the payload is authentic and hasn't been tampered with.
Webhook infrastructure, managed for you
Hookdeck handles ingestion, delivery, observability, and error recovery — so you don't have to.