Fix guide · critical · slack_signing_secret
Slack signing secret in client code
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
- Rotate the signing secret. Slack App settings → Basic Information → App Credentials → "Regenerate" next to Signing Secret.
- Update your environment variables. Same coordination as GitHub: production handler must read the new secret before any new request arrives.
- 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.
- 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