The endpoint URL is not the security boundary

Webhook endpoints are convenient specifically because they are reachable over the open internet. That same property means the URL alone provides no authentication. If the endpoint responds to any well-formed POST request by triggering a workflow step, it responds identically to a real event from the vendor and a fabricated one from anyone who finds the URL in a log, a browser history, or a misconfigured proxy.

This matters more for an AI workflow than for a simple integration, because the workflow does not just log the event. It may draft a response, update a record, or route a case to a person as if the input were verified fact. Treating the payload as ground truth before checking its origin gives an outside actor a direct path into the automation.

Verify the signature, not just its presence

Providers that take webhook security seriously sign every delivery. Stripe includes a `Stripe-Signature` header built from an HMAC-SHA256 hash of the timestamp and raw payload; GitHub does the same with `X-Hub-Signature-256`. In both cases the receiving server recomputes the hash using its own copy of the shared secret and the untouched raw request body, then compares it to the header value.

Two details break this in practice. First, some web frameworks parse and re-serialize the JSON body before your handler sees it, which changes the bytes and makes a correct signature fail verification; the raw body has to reach the verification step unmodified. Second, a naive string equality check on the computed and received hash leaks timing information that an attacker can use to guess the correct value byte by byte, so the comparison needs to run in constant time.

  • Read the raw request body before any JSON parsing or middleware touches it.
  • Recompute the HMAC with the shared secret kept in your own secrets store, never in the repository.
  • Compare hashes with a constant-time function, not `==` or `.equals()`.
  • Reject the request outright on any verification failure; do not fall back to processing it anyway.

A valid signature only proves origin, not freshness

Signature verification confirms the payload came from the provider and was not altered in transit. It does not confirm the request is new. An attacker who intercepts a legitimate, correctly signed delivery can resend it later, and the signature will still check out, a classic replay attack.

The fix is to sign a timestamp along with the payload and reject anything outside a tight tolerance window, which is why Stripe embeds the timestamp inside the signed string itself. Pair that with tracking the provider's event ID for a bounded retention period so a duplicate delivery, replayed or simply retried by the provider after a slow response, is recognized and skipped rather than processed twice.

Put the check before the workflow, not inside it

Signature verification belongs in the thin layer that receives the HTTP request, before any orchestration logic, model call, or write-back runs. If verification is treated as just another workflow step that can be skipped, retried around, or bypassed in a test path left enabled in production, it stops functioning as a security boundary.

The OWASP webhook security guidance frames this the same way: authenticate the sender, enforce a replay window, and validate the payload shape, all before the receiving application does anything with the data. For an AI workflow specifically, that ordering is what keeps a forged event from ever reaching the model or the tool calls it can trigger.