Skip to content
RESEARCH INDEX BREACHROAD / INTELLIGENCE NOTE

Webhook security: signatures, replay and SSRF

Webhooks have two sides and two attack surfaces. How to verify signatures, block replay, design idempotency and avoid building SSRF on request.

PUBLIC RESEARCH
AUTHOR
/ CEO of Breachroad · OSCP · PNPT
PUBLISHED
25 July 2026
READING TIME
17 min read
TOPIC
Penetration Testing and AppSec
Webhook security: signatures, replay and SSRF

A webhook is the one endpoint you deliberately invite a foreign system to call — and at the same time the one feature where your server makes a request to an arbitrary address on a user’s instruction. Those are two entirely different attack surfaces, and most material on the topic blends them, which sends half the advice in the wrong direction.

This article separates the roles. First receiving webhooks, where your application consumes events from an external system. Then sending, where your application lets customers register an address and makes requests to it.

Part one: receiving webhooks

Signatures computed over the raw body

The primary sender authentication mechanism is a signature, usually a message authentication code computed with a shared secret and sent in a header. The key implementation detail: verify the signature over the raw bytes of the request body, exactly as received.

The most common mistake is letting the framework parse the body into a data structure, serialising it back and computing the signature over the result. A different key order, different number formatting or a whitespace character is enough for the result to stop matching — and if it happens to match, that only means it works for the current library version. In many frameworks, access to the raw body requires deliberate configuration, because the default stream is consumed by the parser.

The second detail is constant-time comparison. An ordinary string comparison stops at the first difference and in theory leaks information about a correct prefix. The cost of using the proper comparison function is zero, so there is no reason not to.

The third is per-sender keys and versioning. The signature header should carry a scheme version identifier so that secret rotation and algorithm changes are possible without downtime: for a while you accept two signatures, then retire the old one.

Replay and the time window

A valid signature does not mean the message is fresh. A request captured or replayed from logs stays valid indefinitely if the signature covers only the body.

The answer has two parts. First, a timestamp covered by the signature, and rejection of requests outside a narrow tolerance window measured in minutes. Second, a registry of event identifiers kept at least as long as the tolerance window, so the same event is not processed twice.

Idempotency is a requirement, not an optimisation

Practically every provider guarantees at-least-once delivery, which means duplicates are normal operation rather than a fault. A retry after a timeout, a retry after a network error, a manual queue replay — all of them produce repeats.

The consumer must therefore be idempotent: the event identifier is written in the same transaction as the business effect, and a repeat is skipped. Without that, a “charge the account” event sometimes gets processed twice and the conversation moves from security to refunds.

Remember too that delivery order is not guaranteed. An older update can arrive after a newer one. Resilience comes from an object version number or event timestamp, not from assuming the queue is ordered.

Trusting event content

Even a correctly signed event only tells you the sender sent it. For operations with financial consequences or permission changes, the safer pattern is a thin payload: the event carries an identifier and a type, and the application queries the provider’s API for the object’s current state. That removes the need to trust the message body and, as a bonus, keeps sensitive data out of request bodies and logs.

It also solves the stale event problem: fetched state is always current, regardless of when the event arrived.

Additional layers and their limits

Restricting source addresses to the provider’s ranges is sometimes useful but rarely sufficient: providers change ranges, use content delivery networks, and traffic can be relayed by intermediaries. Treat it as an extra layer, never as a substitute for signatures.

Mutual certificate authentication is stronger than a shared secret and is worth considering in high-value integrations, though the operational cost is higher.

A secret embedded in the webhook URL — “nobody knows this path” — is not authentication. URLs end up in intermediary logs, monitoring systems and support tickets.

Overload resilience

A webhook endpoint is public, so ordinary rules apply: body size limits, rate limits, a hard processing timeout. Good practice is respond fast, process asynchronously: verify the signature, enqueue the event, return a success code, and run the logic outside the request cycle. That shortens response times, reduces provider retries and contains the impact of expensive operations.

Watch the logs as well. Full event bodies get stored “for diagnostics” and contain personal data or payment details. Signature and authentication headers should be redacted by default.

Part two: sending webhooks

Here the situation inverts. The customer registers an address and your server makes requests to it. This is SSRF by design — the feature is precisely that the server connects to a user-supplied address. The whole craft lies in stopping that feature from reaching where it should not.

Preventing access to the internal network

Validating the address string alone is not enough. You must resolve the name and check the destination address before connecting: reject loopback, private ranges, link-local addresses — including the cloud metadata service address — special-purpose ranges and their IPv6 equivalents, including IPv4-mapped addresses.

Then comes a problem that is easy to forget: between the check and the connection, the name can be resolved again to a different address. That is DNS rebinding, and resisting it requires either connecting to the specific previously validated address or re-validating at the socket level.

Redirects are another door: an external address responds with a redirect to an internal one. It is safest not to follow redirects at all, and where that is impossible, to validate every subsequent address exactly like the first and cap the hop count.

The layer that most effectively closes the topic is a dedicated network egress: webhook traffic leaves through a separate component with no access to the internal network, no cloud credentials and no visibility of the metadata service. Then a validation bug stops being critical.

Add the obvious restrictions: HTTPS only, standard port only, no non-network schemes, a hard timeout, a response size limit, and never returning the response body to the user. That last rule matters, because it turns any residual SSRF into a blind variant.

Verifying address ownership

When an address is registered, run a challenge proving the registrant controls it: send a control event containing a random value and expect it echoed back, or ask for a confirmation in the response. Without that, your infrastructure can be used to generate traffic towards other people’s systems and, at sufficient scale, to run a denial of service attack by proxy.

Signing what you send

Since you expect signatures from providers, sign your own events too. The minimum standard is a message authentication code over the raw body, a timestamp covered by the signature, an event identifier and a signature scheme version. Publish verification instructions in your documentation and plan secret rotation with a window during which two keys are valid.

Standardisation helps here. The HTTP message signatures specification describes a provider-independent mechanism, and initiatives standardising webhook formats make correct consumer implementations easier. The fewer bespoke schemes, the fewer mistakes on the receiving side.

Retries and disabling endpoints

Retries should use increasing intervals, a bounded attempt count and an event time-to-live. An endpoint returning errors for an extended period should be disabled automatically with a notification to its owner — otherwise your system will hammer someone else’s server for months.

Limit concurrency per recipient as well. Without it, a single integration can generate traffic indistinguishable, from the recipient’s point of view, from an attack.

Testing methodology

On the receiving side check, in order: whether a request without a signature is rejected; whether a signature computed with a different secret is rejected; whether modifying a single body byte invalidates the signature; whether an hour-old request is rejected; whether replaying the same event produces the business effect twice; whether reordering body fields affects verification; and whether processing happens before the response and how the endpoint behaves with a large body.

On the sending side check whether registering an address pointing at loopback, a private range or the metadata service is rejected; whether a domain name resolving to an internal address is blocked; whether a redirect to an internal address is stopped; whether the recipient’s response body is exposed to the user; whether time, size and attempt limits exist; and whether the address is verified at registration.

Run all attempts in your own test environment or within an agreed scope — testing outbound SSRF in production tends to raise alerts and open an incident on the network side.

Detection

On the receiving side, monitor rejected signatures and rejections due to staleness. A sudden rise in the former means either a botched secret rotation or an impersonation attempt. Monitor duplicate counts too; a rise there can indicate replay of old requests.

On the sending side the most valuable signal is connection attempts to private ranges and to the metadata service address. In a properly designed system such an event should never occur, so every one is an alert. Also watch recipients with unusually high error rates and addresses registered in bulk from a single account.

Event versioning and schema changes

Webhooks are a contract that is harder to change than an API your clients poll, because you initiate the connection and you do not know how the recipient parses the body. A change that would be minor in an API can break integrations for dozens of customers at once.

The rules that keep this manageable are simple. The event type and schema version are explicit in the payload and in a header, so recipients can branch their logic. Adding fields is a backward-compatible change and requires consumers only to ignore unknown keys — state that explicitly in your documentation. Removing a field, changing a value’s type or changing a field’s meaning is a breaking change and requires a new event version plus a period during which both are sent.

It is also worth letting recipients choose which event types they want to receive. That is not merely convenience: a subscription narrowed to the necessary types reduces how much data leaves your system and limits the impact of a misconfigured address.

Payload content is a separate decision. A full payload is convenient but means business data travels to a customer-controlled address and lingers in logs along the way. A thin payload — identifier, type, timestamp — requires an extra call from the recipient, but it markedly shrinks the leak surface and makes compatibility easier, because the schema stays stable.

Checklist

Signatures are computed over raw body bytes and compared in constant time. The signature covers a timestamp and requests outside the tolerance window are rejected. Event identifiers are stored and processing is idempotent. Event order is not assumed. Financially significant operations are confirmed by querying the source. The endpoint enforces size, rate and time limits, and processing is asynchronous. Event bodies and authentication headers are redacted in logs. Recipient addresses are validated after name resolution, with internal ranges blocked. Redirects are blocked or re-validated. Outbound traffic uses a dedicated egress without credentials. Recipient response bodies never reach the user. Address registration requires ownership verification. Outgoing events are signed and secrets rotate with an overlap window.

Conclusion

Webhooks look like a simple mechanism — an HTTP request in the other direction — and that is exactly why they get implemented without a threat model. Yet on the receiving side they carry every message authentication problem there is, and on the sending side they are SSRF built in from the factory, which has to be deliberately constrained.

Two decisions deliver the most: signature verification over the raw body together with a timestamp and an event registry, and a dedicated network egress for outbound traffic. The first closes impersonation and replay; the second ensures that even a validation bug does not lead into your infrastructure.


Primary sources: RFC 9421 — HTTP Message Signatures, OWASP — Server Side Request Forgery Prevention Cheat Sheet, Standard Webhooks.

SHARE / COPY