Skip to content

fix: reject empty signingSecret to prevent involuntary signature bypass#2946

Merged
zimeg merged 4 commits into
mainfrom
fix-accepts-empty-signingSecret
May 27, 2026
Merged

fix: reject empty signingSecret to prevent involuntary signature bypass#2946
zimeg merged 4 commits into
mainfrom
fix-accepts-empty-signingSecret

Conversation

@WilliamBergamin
Copy link
Copy Markdown
Contributor

Summary

  • Reject empty signingSecret ('') at initialization across all receivers and in the request verification layer to prevent accidental verification bypass
  • Add a shared verifySigningSecret() helper that validates the signing secret is a non-empty string when signature verification is enabled
  • Add comprehensive unit tests for the new validation across App, HTTPReceiver, ExpressReceiver, AwsLambdaReceiver, verifyRequest, and verifySigningSecret

Motivation

Previously, passing an empty string ('') as signingSecret did not trigger an error, the value is truthy enough to pass === undefined checks but propagates to createHmac("sha256", ""), which produces a valid HMAC keyed with an empty secret. This would bypass the request verification without warning the user.

The proper way to disable signature verification is by setting signatureVerification: false

Reproduction

Server with issue (app.js):

import { App } from '@slack/bolt';

const app = new App({
  signingSecret: '',
  port: 3000,
  authorize: async () => ({ botToken: 'xoxb-fake', botId: 'B000', botUserId: 'U000' }),
});

app.event('app_mention', async ({ event }) => {
  console.log(`Received app_mention from ${event.user}: ${event.text}`);
});

await app.start();
console.log('Vulnerable server listening on :3000');

Forged request:

TIMESTAMP=$(date +%s) && \
BODY='{"type":"url_verification","challenge":"bypass-confirmed"}' && \
curl -X POST http://localhost:3000/slack/events \
  -H "Content-Type: application/json" \
  -H "x-slack-request-timestamp: $TIMESTAMP" \
  -H "x-slack-signature: v0=$(printf "v0:${TIMESTAMP}:${BODY}" | openssl dgst -sha256 -hmac '' | awk '{print $NF}')" \
  -d "$BODY"

With this fix applied, the App constructor now throws an AppInitializationError and receivers throw a ReceiverInitializationError, preventing the server from starting with an empty signing secret.

Requirements

@WilliamBergamin WilliamBergamin self-assigned this May 27, 2026
@WilliamBergamin WilliamBergamin requested a review from a team as a code owner May 27, 2026 19:56
@WilliamBergamin WilliamBergamin added bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented security labels May 27, 2026
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 27, 2026

🦋 Changeset detected

Latest commit: 28d09d8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@slack/bolt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@zimeg
Copy link
Copy Markdown
Member

zimeg commented May 27, 2026

🧪 Taking a look at failing E2E tests now but the failure seems unrelated to these changes.

@zimeg zimeg added this to the @slack/bolt@next milestone May 27, 2026
Copy link
Copy Markdown
Member

@zimeg zimeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilliamBergamin Such huge thanks for getting this fix set to release 🏁

I left a comment on the Express receiver implementation but that's covered well in testing so we might decide to follow up with changes. For now let's get this merged 🚢 💨

Comment thread src/App.ts
});
}
if (signatureVerification === true && signingSecret === undefined) {
if (signatureVerification === true && !signingSecret) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🌟 praise: Fan of the generic false checks!

Comment on lines +206 to +208
if (typeof signingSecret !== 'function') {
verifySigningSecret(signingSecret, signatureVerification);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔬 question: Is this alright as a best effort check? I'm wondering if we should follow up with an addition that handles the function case?

🗣️ ramble: I notice the empty string case from "function" inputs is caught with more confidence in buildVerificationBodyParserMiddleware so don't consider this a blocker! I'm hoping to mirror implementations across receivers however with these checks all together near the start of this constructor!

Comment on lines 97 to +99
it('should succeed with a token for single team authorization', async () => {
const MockApp = importApp(overrides);
const app = new MockApp({ token: '', signingSecret: '' });
const app = new MockApp({ token: '', signingSecret: 'test-signing-secret' });
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧪 praise: More explicit test cases like this are nice!

@zimeg
Copy link
Copy Markdown
Member

zimeg commented May 27, 2026

🧪 The failing E2E tests are hoped to resolve on main but I'll follow up if changes are needed more!

@zimeg zimeg merged commit 341b60e into main May 27, 2026
26 of 28 checks passed
@zimeg zimeg deleted the fix-accepts-empty-signingSecret branch May 27, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug M-T: confirmed bug report. Issues are confirmed when the reproduction steps are documented security semver:patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants