Your webhook secret is in your client bundle: forge-anything for every provider you use.

2026-05-09 · vibecheck team · 8 min read · Incident response

Quick answer Webhook signing secrets are how your handler proves an inbound webhook is real — Stripe (whsec_*), GitHub (X-Hub-Signature-256), Slack (X-Slack-Signature), Linear, Vercel, Discord apps, anything Svix-powered, anything you self-host. When the signing secret leaks into your client bundle, an attacker can craft and sign forged events that pass your verification step. The forge surface is exactly whatever your handler does — webhooks that trigger deploys, charge confirmations, slash-command actions, repository operations, post-purchase fulfilment. The five-step response: rotate at the provider, audit handler logs for forge-window activity, remove from source, move to runtime config with a provider-specific env var name, never use a single shared WEBHOOK_SECRET across providers.

Webhook secrets are the second-most-leaked credential vibecheck finds, behind Supabase service_role keys. The pattern is consistent: a developer wires up a webhook handler, copies the signing secret from the provider's dashboard into .env, and somewhere along the way the value ends up in a file the bundler picks up. The handler still works — verification passes — but the secret now ships in main.js and any visitor who searches the bundle for whsec_ or WEBHOOK_SECRET finds it.

This post walks the four most common shapes by provider, then the universal incident response, then prevention.

1. Stripe (whsec_*)

The most-detected pattern in the wild because the prefix is stable and the use case is so common. Stripe's webhook signing secret is what stripe.webhooks.constructEvent uses to verify the Stripe-Signature header on incoming webhooks. Every webhook event Stripe ever sent your endpoint — charge.succeeded, customer.subscription.created, invoice.payment_failed, etc. — is signed with this secret.

What an attacker does with it: forges any event your handler accepts. If your handler grants a feature on customer.subscription.created, they craft a forged event for a free email address and your code grants the feature. If your handler refunds on a chargeback notification, they forge a charge-reversed event and your code refunds them. The attacker doesn't need a real Stripe account — they need a curl command and the leaked secret.

Rotation: Stripe Dashboard → Developers → Webhooks → click your endpoint → "Roll signing secret". Stripe sends you the new secret; the old one keeps validating for a 24-hour grace period (so you don't drop legitimate events while you redeploy). Update env vars in production and every preview environment, redeploy, then revoke the old secret manually.

Dedicated fix-guide: /fix/stripe_webhook_secret.

2. GitHub (GITHUB_WEBHOOK_SECRET)

GitHub webhooks have no stable secret prefix — they're whatever string you typed into the secret field when you created the webhook. The signing happens via HMAC-SHA256 with that secret as the key, and your handler verifies via X-Hub-Signature-256.

vibecheck detects this via the variable name (GITHUB_WEBHOOK_SECRET, GH_WEBHOOK_SECRET). The pattern is context-aware because no character distribution distinguishes a 32-character random string from any other random string of that length — only the variable name does.

What an attacker does with it: forges any GitHub event your handler reacts to. Common shapes:

If your repository has a GitHub App webhook (rather than a per-repo webhook), the impact is multi-tenant — every install of the app is using the same signing secret, and every install can be forged against simultaneously.

Rotation: Per-repo webhook: Repository → Settings → Webhooks → click endpoint → edit → generate a new secret. App webhook: App settings → General → Webhook secret → regenerate. There's no grace period — the moment you save, the new secret is what GitHub signs with. Coordinate the env-var update with the rotation.

Dedicated fix-guide: /fix/github_webhook_secret.

3. Slack (SLACK_SIGNING_SECRET)

Slack signs every request to your app's endpoints — slash commands, event subscriptions, interactive payloads, view submissions — using HMAC-SHA256 with your app's signing secret. Verification involves the timestamp from the X-Slack-Request-Timestamp header so handlers can also reject replays.

The signing secret is 32 hex characters; vibecheck flags it via the variable name (SLACK_SIGNING_SECRET) because the raw shape isn't unique enough.

What an attacker does with it:

The handler-level trust model is "if the signature verifies, the request came from Slack." Once the secret leaks, that trust model becomes "the request came from anyone with the secret."

Rotation: Slack App settings → Basic Information → App Credentials → "Regenerate" next to Signing Secret. Updates immediately, no grace period.

Dedicated fix-guide: /fix/slack_signing_secret.

4. Generic WEBHOOK_SECRET / HOOK_SECRET

The catch-all. Linear, Vercel, Cloudflare Pages, Discord apps, Notion, Clerk, anything Svix-powered (Resend, Knock, Hookdeck, Buildkite, etc.), Hookdeck, plus anything you self-host that exposes a webhook surface — all use the same shape: shared secret, HMAC signature in a header, your handler verifies in code.

vibecheck flags variables named WEBHOOK_SECRET or HOOK_SECRET with no provider prefix because the consequence is provider-specific but the leak pattern is generic. grep -B 5 -A 5 WEBHOOK_SECRET src/ on the offending file shows you which provider — usually obvious from the surrounding API client import.

One specific recommendation: use distinct env-var names per provider. If your code has STRIPE_WEBHOOK_SECRET, GITHUB_WEBHOOK_SECRET, and LINEAR_WEBHOOK_SECRET, a single leak rotates one secret. If your code has a single WEBHOOK_SECRET shared across all of them, a single leak forces three rotations and leaves a window where one provider's signing has rotated but another's hasn't.

Dedicated fix-guide: /fix/webhook_secret_generic.

The universal incident response

Same shape as Stripe live key exposure and OpenAI / Anthropic key exposure:

  1. Rotate at the provider's dashboard. Pre-resolved links above per provider.
  2. Update env vars in every environment — production, staging, every preview branch deploy. Coordinate timing so the handler reads the new secret before the next event arrives.
  3. Audit handler logs for the exposure window. Anything your handler did between when the leaked secret was first published and when the rotation completed needs review. For low-traffic webhooks (a few events per day), this is a 5-minute scroll. For high-traffic webhooks (Stripe production, GitHub Apps), pull logs and look for unusual event patterns: bursts of forged events, events with malformed bodies that still validated, events from impossible sources.
  4. Remove the secret from source. git log -S "<first-12-chars>" finds the commit. Inspect surrounding context, delete, push.
  5. Move to runtime config. Cloudflare Workers/Pages: wrangler secret put NAME. Vercel: env var marked "Sensitive". Anywhere: never VITE_* or NEXT_PUBLIC_* prefix — those ship to the browser. The point of moving the secret to runtime is that it's never read at build time, so the bundler can never include it in the output.

Prevention: the architectural rule

Webhook handlers are server-only by definition. No legitimate code path puts the signing secret in a file the bundler reads. Three layers of defence:

  1. Distinct env-var names per provider. Already covered. Reduces blast radius of a single leak.
  2. Build-time check. Add a CI step that greps the bundled output for known webhook patterns. grep -E "WEBHOOK_SECRET|whsec_|SIGNING_SECRET" dist/ in your build script — fail the build on hit.
  3. Pre-commit hook. git-secrets or a custom .git/hooks/pre-commit that rejects commits containing webhook-shaped strings. Catches the leak before push.

How vibecheck fits in

vibecheck's webhook detection covers Stripe (stripe_webhook_secret), GitHub (github_webhook_secret), Slack (slack_signing_secret), the generic shape (webhook_secret_generic), plus the public webhook URL leaks: discord_webhook_url and slack_webhook_url (those are URLs that contain the secret as a path segment — different shape, same consequence).

Run a scan against any deploy. Webhook-related findings appear in the "Other secrets in the bundle" section with full provider-specific remediation links.

Inspect your app for leaked webhook secrets