Practice · Payments · Card 2
Why does this webhook controller skip CSRF?
A controller that takes Stripe webhook posts. skip_before_action :verify_authenticity_token on the first line. Why is that safe?
The code
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def stripe
event = Stripe::Webhook.construct_event(
request.body.read,
request.env["HTTP_STRIPE_SIGNATURE"],
ENV.fetch("STRIPE_WEBHOOK_SECRET")
)
ProcessStripeEventJob.perform_later(event.id)
head :ok
rescue Stripe::SignatureVerificationError
head :bad_request
end
end The question
Why is skipping CSRF safe here, and what's actually preventing an attacker from forging webhook calls?
Take a moment. CSRF protects against one threat model; webhook signatures protect against another. What are they each defending against, and why does skipping one make sense when the other is in place?
Why it's safe
CSRF protection and webhook signature verification defend against different threats:
- CSRF assumes a logged-in user's browser session and a token stored there. It defends against a malicious site tricking that browser into submitting a form to your app.
- Webhook signature defends against an attacker forging a server-to-server POST to your webhook endpoint, where no user session is involved.
Webhooks don't come from a browser session, so the CSRF token wouldn't be present and Rails would reject every legitimate webhook with InvalidAuthenticityToken. Skipping CSRF is required to receive the request at all.
The HMAC signature on the request body, verified with the shared secret, is what proves "Stripe sent this." An attacker who doesn't know the secret can't forge a valid signature. construct_event raises on a bad signature; the controller catches that and returns 400.
Wrong takes worth noticing
- "CSRF can't be skipped safely; this is a bug." Misunderstands what CSRF is for. It assumes a browser-session threat model that doesn't apply here.
- "Stripe is whitelisted by IP." Stripe doesn't guarantee a stable IP range. The protection is the signature, not the source IP.
- "You can rely on the
User-Agentheader to identify Stripe." Trivially spoofable. The signature is the only real proof.
The principle
Two security mechanisms, two threat models. Skipping one is safe only when the other replaces its job. CSRF off + signature verification on is the right pair for webhook endpoints. CSRF off and no signature check is wide open. Always log signature failures — a sustained stream of them is an active attack signal.
Theory
For the full walkthrough of webhook security, idempotency, and replay handling, read Payments · Webhooks: Verify, Idempotent, Replayable.