Fix guide · critical · slack_signing_secret

Slack signing secret in client code

What this rule means

A 32-character hex string assigned to SLACK_SIGNING_SECRET was found in your deployed JavaScript. The signing secret is what proves an inbound Slack request (slash command, event, interactive payload) is real — leak it and any attacker can forge requests to your Slack handler endpoints.

Why it matters

Slack signs every request to your app's endpoints using HMAC-SHA256 with a shared secret. Your handler verifies by recomputing the signature with the same secret and comparing against the X-Slack-Signature header. With the leaked secret, an attacker can craft any payload — /yourcommand drop database, an app_mention event from a forged user, a button-click interactivity payload — and your handler will accept it as having come from Slack.

The attack scope depends entirely on what your Slack handler does. Common shapes: posting messages back to channels via the bot token, calling internal APIs based on slash command arguments, modifying user state from interactivity payloads. Each becomes a forge-anything endpoint once the signing secret leaks.

How to fix it

  1. Rotate the signing secret. Slack App settings → Basic Information → App Credentials → "Regenerate" next to Signing Secret.
  1. Update your environment variables. Same coordination as GitHub: production handler must read the new secret before any new request arrives.
  1. Audit handler logs. Slack requests during the exposure window need review — anything that resulted in a side effect (DB writes, external API calls, message posts) should be checked.
  1. Remove from source + move to runtime config. Same as the GitHub webhook fix — never NEXT_PUBLIC_* / VITE_*.

Verification code:

import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(req: Request) {
  const ts = req.headers.get("x-slack-request-timestamp");
  const sig = req.headers.get("x-slack-signature");
  const body = await req.text();
  if (!ts || !sig) return new Response("missing headers", { status: 401 });

  // Reject replay attacks: drop requests older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return new Response("stale request", { status: 401 });
  }

  const baseString = `v0:${ts}:${body}`;
  const expected = "v0=" +
    createHmac("sha256", process.env.SLACK_SIGNING_SECRET!)
      .update(baseString)
      .digest("hex");
  if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response("invalid signature", { status: 401 });
  }
  // ... process payload
}

Note the timestamp check — Slack signs the timestamp into the base string specifically so handlers can reject replays. Don't skip it.

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