Webhook Security & Signature Verification

Provider-specific webhook signature verification, rate limiting, payload size controls, and replay attack prevention for payment processor integrations.

5 min read Security & Infrastructure Published Mar 11, 2025

Overview

Payment processor webhooks are the primary mechanism for asynchronous event delivery — charge confirmations, payout status updates, refund completions, and dispute notifications. Because these endpoints are publicly accessible and directly affect financial records, CommunityPay applies layered security controls at every webhook entry point.

All provider events enter through one endpoint and pass the same checks, in the same order, before any handler runs. Events for connected accounts are routed by domain to the handler for that kind of event: vendor payments, onboarding, resident payments, bank changes, and payouts.


The Sequence

Before the endpoint runs, the rate-limit middleware applies the per-provider limit described below. The endpoint then applies these steps in order:

  1. Payload size. A request whose Content-Length is over 512 KB is rejected with HTTP 413 before the body is read.
  2. Provider. The provider must be one of the four the endpoint serves. Anything else is rejected with HTTP 400.
  3. Endpoint rate limit. A flat limit of 100 requests a minute per provider and IP applies here, in addition to the middleware limit. A request over it receives HTTP 429.
  4. Signature present. A request with no signature header is rejected with HTTP 401.
  5. Secret configured. If the provider's signing secret is not configured, the request is rejected.
  6. Signature verified. The signature is checked against the raw body before any processing. A failure is rejected with HTTP 401.
  7. Idempotency. The event id is looked up. A repeat of an event already processing or processed is acknowledged and skipped, so a replay does not process twice. A new event is recorded with its type and signature.
  8. Handler. The provider's gateway handler runs. The event's record is marked processed or failed.

Rate Limiting

The WebhookRateLimitMiddleware applies per-provider, per-IP rate limits to all webhook endpoints:

Provider Rate Limit Window
Stripe 100 requests/minute 60 seconds
Dwolla 60 requests/minute 60 seconds
Plaid 50 requests/minute 60 seconds
SES (SNS bounce, complaint, and invoice inbox events) 300 requests/minute 60 seconds
Unknown 30 requests/minute 60 seconds

The middleware:

  1. Detects webhook endpoints by URL path matching (/webhook in path)
  2. Extracts the provider name from the URL structure
  3. Constructs a per-provider, per-IP cache key
  4. Counts requests in a cache entry that expires 60 seconds after the last request it counted
  5. Returns HTTP 429 (Too Many Requests) when limits are exceeded

Rate limit violations are logged with the offending IP address for security monitoring.


Signature Verification

Each provider has its own scheme.

Stripe Verification

Stripe webhooks use a composite signature header (Stripe-Signature) containing a timestamp and one or more HMAC-SHA256 signatures:

  1. Parse the signature header to extract timestamp (t=) and signature values (v1=)
  2. Validate timestamp: Reject if the timestamp differs from the server clock by more than 300 seconds (5-minute replay window)
  3. Compute expected signature: HMAC-SHA256 of {timestamp}.{payload} over the exact raw bytes received, using the webhook signing secret
  4. Compare: Use hmac.compare_digest for constant-time comparison, preventing timing-based attacks

Events from connected accounts are normally signed with a separate signing secret from platform events. The endpoint tries the secret that matches the event's kind first and then the other, so an event signed with either secret verifies. Neither the signatures, the secrets, nor the payload are written to the logs, because anyone reading the logs could otherwise replay a forged event inside the tolerance window.

Dwolla Verification

Dwolla uses a simpler HMAC-SHA256 signature:

  1. Compute HMAC-SHA256 of the raw payload using the webhook secret
  2. Compare against the X-Request-Signature-SHA-256 header using hmac.compare_digest

Plaid Verification

Plaid delivers a JWT in the Plaid-Verification header. Verification of that JWT is not implemented, so the endpoint rejects every Plaid webhook and fails closed. Nothing from Plaid is processed.


Payload Size Limits

  • Maximum: 512 KB, read from the Content-Length header before the body is processed
  • Oversized requests are rejected with HTTP 413
  • A request that sends no Content-Length header is not checked at this step
  • This prevents memory exhaustion attacks on webhook endpoints

Replay Attack Prevention

Two controls work together:

  • A signed timestamp. For Stripe, the signed payload includes the timestamp, so a webhook cannot be replayed with a forged timestamp, and a genuine one older than 300 seconds is rejected.
  • A record of every event id. A repeat of an event id already processing or processed is acknowledged and skipped. A replay inside the window does not process twice.

What a Webhook May and May Not Do

A webhook stamps the record it concerns. It does not post a ledger entry for a vendor payment: the posting happens inside the journal engine, under the enforcement guards, and the processor's event only stamps the payment record with what happened — a payout paid or failed, for example. A failed vendor payout reverses its entry and reopens the bill.

Bank and payout events also trigger a check of the association's pinned payout bank against the processor's default bank, and the same check runs hourly. If they disagree, automatic payouts pause until a person releases them.


Security Properties

Property Mechanism
Authentication HMAC-SHA256 signature per provider; Plaid rejected until verification is implemented
Integrity Signature covers the full raw payload
Freshness Timestamp validation (300s window)
Idempotency Every event id is recorded; a repeat is skipped
Availability Per-provider rate limiting
Size bounds 512 KB limit before processing
Timing safety hmac.compare_digest constant-time comparison
How CommunityPay Enforces This
  • Per-provider rate limits enforced via middleware (Stripe: 100/min, Dwolla: 60/min, Plaid: 50/min)
  • HMAC-SHA256 signature verification with constant-time comparison (hmac.compare_digest), before any processing
  • Replay attack prevention via 300-second timestamp validation window, and every event id recorded so a repeat is skipped
  • Payload size limit: a request over 512 KB is rejected before processing
  • Plaid webhooks are rejected until verification is implemented; the endpoint fails closed
Login