Fix guide · critical · github_webhook_secret
GitHub webhook signing secret in client code
A string assigned to GITHUB_WEBHOOK_SECRET (or similar) was found in your deployed JavaScript. With this secret, an attacker can forge any webhook event GitHub would normally send — pull requests, pushes, comments, releases — and your webhook handler will validate the forged HMAC and process it as legitimate.
Why it matters
GitHub webhooks are HMAC-signed using a shared secret you set when you create the webhook (Repository → Settings → Webhooks → secret, or App → webhook → Secret). Your handler should call crypto.timingSafeEqual against the X-Hub-Signature-256 header and reject mismatches. The shared secret is the only thing that proves an inbound webhook is real — leak it and the verification step becomes a no-op for the attacker.
The attack pattern: leaked secret → attacker crafts a forged pull_request.merged event → your handler runs the merge-side automation (deploy, notify, cleanup) → no actual merge happened in GitHub. Variants exist for any event your handler reacts to.
How to fix it
- Rotate the webhook secret. Repository → Settings → Webhooks → click your endpoint → edit → generate a new secret. For GitHub Apps: App settings → General → Webhook secret → regenerate.
- Update your environment variables. Set the new secret in every environment your handler runs in (production, staging, preview deploys). The webhook handler must read the new value before any new event arrives — coordinate the rotation timing so you don't have a window where GitHub signs with the new secret but your handler still verifies with the old one.
- Audit handler logs for the exposure window. Anything your webhook handler did between when the leaked secret was first published and when you rotated should be reviewed. Particularly: deploys triggered, branches created/deleted, PR comments posted, repository operations.
- Remove the secret from source.
git log -S "<first-12-chars>"finds the commit. Inspect, delete, push.
- Move secret to runtime config. Cloudflare Workers / Pages:
wrangler secret put GITHUB_WEBHOOK_SECRET. Vercel: env var marked "Sensitive". Anywhere: neverVITE_*/NEXT_PUBLIC_*prefix — those ship to the browser.
Your handler should look like:
import { createHmac, timingSafeEqual } from "node:crypto";
export async function POST(req: Request) {
const sig = req.headers.get("x-hub-signature-256");
const body = await req.text();
const expected = "sha256=" +
createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET!)
.update(body)
.digest("hex");
if (!sig || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return new Response("invalid signature", { status: 401 });
}
// ... process event
}
Full guide: /blog/webhook-secrets-leaked.
Did vibecheck flag this on your app?
If you reached this page from a vibecheck inspection report, the redacted match in your scan output is the exact string we found in your bundle. After applying the fix above, run the inspection again — the finding should clear.
Run another inspection