Verifying Webhook Signatures Inside an MCP Server
When an AI agent acts on a webhook, an unverified payload is an injection vector. Here's how HMAC webhook signatures work (Stripe, GitHub), why timing-safe comparison matters, and how to verify a callback from inside an MCP server before the agent acts.
Ozer
Developer & Founder of HookSense
When a human reads a webhook in a dashboard, a forged payload is a curiosity. When an AI agent acts on one — ships the deploy, refunds the charge, approves the request — a forged payload is a command injection. Anyone who learns your callback URL could steer the agent. That's why signature verification matters more, not less, in the agent era.
This post explains how HMAC webhook signatures work, why the comparison has to be timing-safe, and how to verify a callback from inside an MCP server before your agent trusts it.
Why signatures matter for agents specifically
A callback URL is a secret-ish capability, but URLs leak — into logs, proxies, screenshots, an agent's own transcript. Treat "knows the URL" as "the public." A signature is the real proof: the sender computes an HMAC of the exact payload using a shared secret, and you recompute it on arrival. If they match, the payload is authentic and untampered. If your agent acts without that check, you've wired an unauthenticated instruction straight into a tool-using loop.
How HMAC webhook signatures work
The sender and you share a secret. For each delivery the sender sends a header containing an HMAC (usually SHA-256) of the raw request body. Two common shapes:
- Stripe — header
Stripe-Signature: t=1730000000,v1=5257a8.... You sign the string{timestamp}.{raw_body}and compare thev1value; the timestamp also guards against replay (reject if it's too old). - GitHub — header
X-Hub-Signature-256: sha256=7d38cd.... You sign the raw body and compare after thesha256=prefix.
Same idea, different envelopes: recompute the HMAC over the bytes you received and check it against what arrived.
The raw-body gotcha
The single most common verification bug: signing the parsed body instead of the raw bytes. The signature is computed over the exact bytes sent. If your framework parses JSON and you re-serialize it, key order and whitespace change, the bytes change, and a valid signature fails.
// WRONG — re-serialized JSON, bytes differ from what was signed
verify(JSON.stringify(parsedBody), signature, secret)
// RIGHT — verify against the exact raw body
verify(rawBodyBuffer, signature, secret)
Why the comparison must be timing-safe
Once you've recomputed the expected HMAC, you compare it to the one that arrived. A normal === short-circuits at the first differing character — so a forged signature that gets the first few characters right returns very slightly slower to fail. Over many attempts, that timing difference leaks how much of the prefix matched, letting an attacker reconstruct a valid signature byte by byte.
A timing-safe comparison (Node's crypto.timingSafeEqual) always takes the same time regardless of where the strings differ, closing the side channel. Always use it for signatures, tokens, and any secret comparison.
Verifying from inside an MCP server
Doing all of this — picking the provider scheme, recomputing against the raw body, timing-safe compare, replay window — inside every agent is error-prone. HookSense does it at ingest and exposes a verify_signature tool so the agent can confirm authenticity explicitly before acting:
// configure the endpoint's secret once, then:
verify_signature({ slug: "payments-prod", requestId: "evt_abc" })
-> { valid: true, provider: "stripe" }
Under the hood it runs a timing-safe HMAC check against the endpoint's configured secret (Stripe, GitHub, Shopify, or custom). The agent's rule becomes simple: if not valid, don't act.
Checklist for agent-safe callbacks
- Verify the HMAC against the raw body, not a re-serialized copy.
- Use a timing-safe comparison.
- Honor the provider's replay window (reject stale timestamps).
- Use the event ID for idempotency — retries resend the same event.
- Make "valid signature" a precondition for any agent action.
Want this handled for you? HookSense verifies and decrypts every callback before your agent sees it, and the MCP server ships verify_signature out of the box. Start at hooksense.com — see also How AI Agents Receive Async Callbacks Without Polling.
Related posts
Related terms
Try HookSense Free
Inspect, debug, and replay webhooks in real-time. No credit card required.
Get Started Free