Inbound Webhooks Need Signature Checks
Slack, GitHub, Discord, Stripe, and other platforms send high-trust events to public endpoints. Signature verification, replay windows, raw-body handling, and idempotency decide whether a webhook is evidence or attacker input.
A webhook endpoint is usually designed to be reachable from the public internet. That is useful for automation, but it means the endpoint has to treat every inbound request as untrusted until the sender and payload are verified. A secret-looking URL, a familiar JSON shape, or a source IP that looks reasonable is not enough when the request can trigger role changes, deployments, billing actions, incident alerts, invite creation, or moderation workflows.
The practical control is request verification before business logic. Slack signs requests with an app signing secret, GitHub signs deliveries with a webhook secret, Discord requires Ed25519 validation for interaction endpoints, Stripe signs event payloads, and Amazon SNS signs messages for HTTP endpoints. The details differ, but the pattern is consistent: preserve the raw body, validate the signature, reject stale or replayed requests, process each event idempotently, and keep authorization checks above the transport proof.
Key Takeaways
- check_circle A webhook URL is a routing address, not a complete authentication mechanism.
- check_circle Signature verification has to run before parsing, queuing, role updates, deploys, billing changes, or message sends.
- check_circle Raw request bodies matter because many providers sign the exact bytes they sent, not the JSON object after a framework rewrites it.
- check_circle Replay protection and duplicate handling are separate controls. A valid signed request can still be old or already processed.
- check_circle A provider signature proves origin and integrity, not whether the user, workspace, repository, guild, or account should be allowed to trigger the action.
- check_circle Each provider, environment, and high-risk event path should have its own secret, rotation plan, and failure logging.
The Endpoint Is Public By Design
Webhooks exist because one service needs to notify another without a user sitting in front of both systems. Slack sends slash commands and Events API callbacks. GitHub sends repository, organization, and GitHub App events. Discord sends interaction payloads to an application endpoint. Stripe sends payment and subscription events. Amazon SNS can deliver notifications and subscription confirmations to HTTP endpoints. For a community or collaboration product, those requests often sit near the control plane.
That is the security tension. The endpoint has to be reachable enough for the provider to call it, but it may also be powerful enough to grant roles, publish announcements, open incident tickets, start builds, update CRM records, grant paid access, or remove a member. Attackers do not need to break the provider if they can send a plausible request directly to the receiver and the receiver trusts the shape of the payload.
A Secret URL Is Not Enough
Some teams still treat a webhook path as a secret because it contains a long random token. That can help reduce noise, but it is brittle as the only control. URLs appear in reverse-proxy logs, analytics records, crash reports, CI output, screenshots, browser history, documentation, support tickets, and chat messages. They are also easy to reuse by accident across development, staging, and production.
Source IP filtering is useful where a provider maintains stable published ranges, but it is not a substitute for payload authentication. Network controls can be misconfigured, bypassed by trusted infrastructure, or lost behind a proxy. Signature verification binds the payload to a provider-held secret or public key. It gives the receiver a stronger answer to the first question: did this exact message come from the expected sender without modification?
Verify Before Trusting The Payload
Provider implementations vary, but the order of operations should be boring. Capture the raw request body. Read the signature and timestamp headers. Recreate the provider's signed message exactly as documented. Validate the HMAC or public-key signature with the right secret or public key. Compare values with a constant-time comparison where that applies. Reject the request before any side effect if verification fails.
The raw-body requirement is where many otherwise careful integrations fail. A web framework may parse JSON, normalize whitespace, reorder fields, transcode text, or consume the body stream before verification code runs. Stripe explicitly warns that manipulating the raw body breaks verification. Slack signs a basestring that includes the timestamp and raw body. GitHub signs the payload contents. Discord signs the timestamp concatenated with the body. Verification middleware should sit at the edge of the route, before generic body parsing changes the bytes.
Replay Windows And Duplicate Events
A correct signature proves that the request was valid when it was created. It does not prove that the request is fresh. Slack and Stripe both use timestamps as part of replay protection, and Discord includes a timestamp header for interaction verification. Receivers should reject old requests outside a narrow tolerance, keep clocks synchronized, and alert on repeated stale attempts because a captured signed payload can be replayed if the receiver only checks the signature.
Duplicate delivery is a different problem. Providers retry when a receiver times out or returns an error, and operators can manually resend some events. Stripe tells webhook handlers to guard against duplicated event receipts. GitHub has delivery identifiers. The receiver should store event IDs, make actions idempotent, and separate acknowledgement from slow work. A queue is usually safer than doing complex business logic inside the request-response path.
Scope Secrets Like Production Credentials
Webhook signing secrets are production credentials. They should live in a secret manager or protected environment variable, not in source code, test fixtures, or a shared wiki. A single shared secret across providers or environments makes rotation hard and expands the blast radius. Development, staging, and production endpoints should have separate secrets, and high-risk event families may deserve separate endpoints so access can be reviewed and revoked independently.
Rotation has to be rehearsed. Some providers support overlapping secrets for a short period; others require an immediate cutover. The receiver should expose a clear deployment path for accepting a new secret, logging which version verified a request, and removing the old one after the provider stops signing with it. A leaked webhook secret should trigger the same response discipline as a leaked bot token: rotate, inspect logs, replay test critical actions, and look for forged or unusual event patterns.
Testing And Incident Response
Signature checks need negative tests. Send a request with no signature, an altered body, an old timestamp, a wrong secret, a wrong public key, a valid signature for the wrong endpoint, and a valid event from the wrong tenant. Confirm that every case fails before queueing or side effects. Discord says it performs automated checks that include invalid signatures for interaction endpoints; teams should run their own checks for every provider.
Logging should capture enough to investigate without storing the secret or sensitive payload content. Record provider, endpoint, tenant, event ID, timestamp, verification result, reason for rejection, and request correlation ID. After suspicious activity, rotate secrets, compare accepted events with provider delivery logs, identify downstream actions, and make sure retries do not recreate the bad state. The goal is not only to block forged requests, but to prove which requests were accepted and why.
Checklist
- Verify provider signatures before parsing or processing webhook payloads.
- Preserve the raw request body for verification and test that middleware does not rewrite it.
- Reject missing signatures, invalid signatures, stale timestamps, unknown tenants, and unsupported event types.
- Store processed event IDs and make side effects idempotent so retries do not duplicate actions.
- Use separate secrets for each provider, environment, and high-risk endpoint.
- Log verification failures and rotate webhook secrets with the same discipline used for bot tokens.
Sources
- Slack Developer Docs: Verifying requests from Slack open_in_new
- GitHub Docs: Validating webhook deliveries open_in_new
- Discord Developer Docs: Interactions overview open_in_new
- Stripe Docs: Receive Stripe events in your webhook endpoint open_in_new
- AWS Docs: Verifying the signatures of Amazon SNS messages open_in_new
- RFC 2104: HMAC open_in_new
- RFC 8032: Edwards-Curve Digital Signature Algorithm open_in_new
Continue Reading
Break-Glass Admin Accounts Need A Runbook
Community platforms and SaaS workspaces need emergency admin access that works during lockouts, outages, founder turnover, and MFA failures. The hard part is keeping that access usable without making it an attacker shortcut.
Mobile App Logs Can Become Private Chat Data
Debug logs, crash reports, and support bundles can capture identifiers, tokens, message snippets, and device context. Chat apps need logging policy before troubleshooting becomes data exposure.
Redirect URIs Are OAuth Trust Boundaries
Community apps for Slack, Discord, GitHub, and identity providers often fail at the callback layer. Exact redirect matching, state, PKCE, and callback ownership decide where tokens land.