From 6c70632f678caae0c204ca458417570463a27979 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 12:22:58 -0700 Subject: [PATCH 1/5] fix: defer SignatureVerifier construction so Socket Mode apps init without a signing secret slack_sdk>=3.43.0 validates the signing secret when a SignatureVerifier is constructed, raising "ValueError: signing_secret must not be empty." App.__init__ eagerly builds the RequestVerification middleware (and thus a SignatureVerifier) for every app, including Socket Mode apps that have no signing secret, so those apps now fail to initialize. Construct the SignatureVerifier lazily on first use instead. Request verification is already skipped for Socket Mode requests, so the verifier is never built for them. HTTP requests still require a valid signing secret as before. Fixes #1535 Co-Authored-By: Claude --- .../request_verification/request_verification.py | 13 ++++++++++++- tests/scenario_tests/test_app.py | 10 ++++++++++ .../test_request_verification.py | 14 ++++++++++++++ .../test_request_verification.py | 15 +++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/slack_bolt/middleware/request_verification/request_verification.py b/slack_bolt/middleware/request_verification/request_verification.py index c0f3f5c31..aeae7255c 100644 --- a/slack_bolt/middleware/request_verification/request_verification.py +++ b/slack_bolt/middleware/request_verification/request_verification.py @@ -20,9 +20,20 @@ def __init__(self, signing_secret: str, base_logger: Optional[Logger] = None): signing_secret: The signing secret base_logger: The base logger """ - self.verifier = SignatureVerifier(signing_secret=signing_secret) + # The verifier is created lazily so that apps without a signing secret + # (e.g. Socket Mode) can be initialized. slack_sdk>=3.43.0 rejects an + # empty signing secret on construction, but request verification is + # skipped for those requests anyway (see `_can_skip`). + self._signing_secret = signing_secret + self._verifier: Optional[SignatureVerifier] = None self.logger = get_bolt_logger(RequestVerification, base_logger=base_logger) + @property + def verifier(self) -> SignatureVerifier: + if self._verifier is None: + self._verifier = SignatureVerifier(signing_secret=self._signing_secret) + return self._verifier + def process( self, *, diff --git a/tests/scenario_tests/test_app.py b/tests/scenario_tests/test_app.py index 9fe6f423f..c003fc476 100644 --- a/tests/scenario_tests/test_app.py +++ b/tests/scenario_tests/test_app.py @@ -96,6 +96,16 @@ def test_token_verification_enabled_False(self): assert self.received_requests.get("/auth.test") is None + def test_socket_mode_app_without_signing_secret(self): + # A Socket Mode app has no signing secret. Initializing the app must not + # raise, even though slack_sdk>=3.43.0 rejects an empty signing secret + # when the request verification middleware builds its SignatureVerifier. + app = App( + client=self.web_client, + token_verification_enabled=False, + ) + assert app is not None + # -------------------------- # multi teams auth # -------------------------- diff --git a/tests/slack_bolt/middleware/request_verification/test_request_verification.py b/tests/slack_bolt/middleware/request_verification/test_request_verification.py index ae163a84d..0ecb61fd1 100644 --- a/tests/slack_bolt/middleware/request_verification/test_request_verification.py +++ b/tests/slack_bolt/middleware/request_verification/test_request_verification.py @@ -60,3 +60,17 @@ def test_ssl_check_param_requires_valid_signature(self): resp = middleware.process(req=req, resp=resp, next=next) assert resp.status == 401 assert resp.body == """{"error": "invalid request"}""" + + def test_empty_signing_secret_does_not_raise_on_init(self): + # A Socket Mode app has no signing secret. Constructing the middleware + # must not raise, even though slack_sdk>=3.43.0 rejects an empty + # signing secret when the SignatureVerifier is created. + RequestVerification(signing_secret="") + + def test_socket_mode_request_skips_verification_without_signing_secret(self): + middleware = RequestVerification(signing_secret="") + req = BoltRequest(mode="socket_mode", body="payload={}", headers={}) + resp = BoltResponse(status=404, body="default") + resp = middleware.process(req=req, resp=resp, next=next) + assert resp.status == 200 + assert resp.body == "next" diff --git a/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py b/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py index 28921bc87..97898e030 100644 --- a/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py +++ b/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py @@ -66,3 +66,18 @@ async def test_ssl_check_param_requires_valid_signature(self): resp = await middleware.async_process(req=req, resp=resp, next=next) assert resp.status == 401 assert resp.body == """{"error": "invalid request"}""" + + def test_empty_signing_secret_does_not_raise_on_init(self): + # A Socket Mode app has no signing secret. Constructing the middleware + # must not raise, even though slack_sdk>=3.43.0 rejects an empty + # signing secret when the SignatureVerifier is created. + AsyncRequestVerification(signing_secret="") + + @pytest.mark.asyncio + async def test_socket_mode_request_skips_verification_without_signing_secret(self): + middleware = AsyncRequestVerification(signing_secret="") + req = AsyncBoltRequest(mode="socket_mode", body="payload={}", headers={}) + resp = BoltResponse(status=404, body="default") + resp = await middleware.async_process(req=req, resp=resp, next=next) + assert resp.status == 200 + assert resp.body == "next" From d82e028286717d6642d20fa92cb03eb3b0db9e70 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 12:36:11 -0700 Subject: [PATCH 2/5] test: assert HTTP requests still require a non-empty signing secret Add sync and async tests confirming that request verification still raises for an HTTP request when the signing secret is empty, so the lazy verifier cannot be weakened into silently accepting unverified requests. Drop the now-redundant explanatory comments. Co-Authored-By: Claude --- .../request_verification/request_verification.py | 4 ---- tests/scenario_tests/test_app.py | 3 --- .../request_verification/test_request_verification.py | 11 ++++++++--- .../request_verification/test_request_verification.py | 11 ++++++++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/slack_bolt/middleware/request_verification/request_verification.py b/slack_bolt/middleware/request_verification/request_verification.py index aeae7255c..cae8315e6 100644 --- a/slack_bolt/middleware/request_verification/request_verification.py +++ b/slack_bolt/middleware/request_verification/request_verification.py @@ -20,10 +20,6 @@ def __init__(self, signing_secret: str, base_logger: Optional[Logger] = None): signing_secret: The signing secret base_logger: The base logger """ - # The verifier is created lazily so that apps without a signing secret - # (e.g. Socket Mode) can be initialized. slack_sdk>=3.43.0 rejects an - # empty signing secret on construction, but request verification is - # skipped for those requests anyway (see `_can_skip`). self._signing_secret = signing_secret self._verifier: Optional[SignatureVerifier] = None self.logger = get_bolt_logger(RequestVerification, base_logger=base_logger) diff --git a/tests/scenario_tests/test_app.py b/tests/scenario_tests/test_app.py index c003fc476..5cdff39bf 100644 --- a/tests/scenario_tests/test_app.py +++ b/tests/scenario_tests/test_app.py @@ -97,9 +97,6 @@ def test_token_verification_enabled_False(self): assert self.received_requests.get("/auth.test") is None def test_socket_mode_app_without_signing_secret(self): - # A Socket Mode app has no signing secret. Initializing the app must not - # raise, even though slack_sdk>=3.43.0 rejects an empty signing secret - # when the request verification middleware builds its SignatureVerifier. app = App( client=self.web_client, token_verification_enabled=False, diff --git a/tests/slack_bolt/middleware/request_verification/test_request_verification.py b/tests/slack_bolt/middleware/request_verification/test_request_verification.py index 0ecb61fd1..53af43bf3 100644 --- a/tests/slack_bolt/middleware/request_verification/test_request_verification.py +++ b/tests/slack_bolt/middleware/request_verification/test_request_verification.py @@ -1,5 +1,6 @@ from time import time +import pytest from slack_sdk.signature import SignatureVerifier from slack_bolt.middleware import RequestVerification @@ -62,9 +63,6 @@ def test_ssl_check_param_requires_valid_signature(self): assert resp.body == """{"error": "invalid request"}""" def test_empty_signing_secret_does_not_raise_on_init(self): - # A Socket Mode app has no signing secret. Constructing the middleware - # must not raise, even though slack_sdk>=3.43.0 rejects an empty - # signing secret when the SignatureVerifier is created. RequestVerification(signing_secret="") def test_socket_mode_request_skips_verification_without_signing_secret(self): @@ -74,3 +72,10 @@ def test_socket_mode_request_skips_verification_without_signing_secret(self): resp = middleware.process(req=req, resp=resp, next=next) assert resp.status == 200 assert resp.body == "next" + + def test_http_request_with_empty_signing_secret_raises(self): + middleware = RequestVerification(signing_secret="") + req = BoltRequest(body="payload={}", headers={}) + resp = BoltResponse(status=404) + with pytest.raises(ValueError): + middleware.process(req=req, resp=resp, next=next) diff --git a/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py b/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py index 97898e030..126b04f94 100644 --- a/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py +++ b/tests/slack_bolt_async/middleware/request_verification/test_request_verification.py @@ -68,9 +68,6 @@ async def test_ssl_check_param_requires_valid_signature(self): assert resp.body == """{"error": "invalid request"}""" def test_empty_signing_secret_does_not_raise_on_init(self): - # A Socket Mode app has no signing secret. Constructing the middleware - # must not raise, even though slack_sdk>=3.43.0 rejects an empty - # signing secret when the SignatureVerifier is created. AsyncRequestVerification(signing_secret="") @pytest.mark.asyncio @@ -81,3 +78,11 @@ async def test_socket_mode_request_skips_verification_without_signing_secret(sel resp = await middleware.async_process(req=req, resp=resp, next=next) assert resp.status == 200 assert resp.body == "next" + + @pytest.mark.asyncio + async def test_http_request_with_empty_signing_secret_raises(self): + middleware = AsyncRequestVerification(signing_secret="") + req = AsyncBoltRequest(body="payload={}", headers={}) + resp = BoltResponse(status=404) + with pytest.raises(ValueError): + await middleware.async_process(req=req, resp=resp, next=next) From 540c61ac5f3c3de5f8014faa709efc58f00f59ce Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 12:45:20 -0700 Subject: [PATCH 3/5] docs: explain why the SignatureVerifier is built lazily Co-Authored-By: Claude --- .../middleware/request_verification/request_verification.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/slack_bolt/middleware/request_verification/request_verification.py b/slack_bolt/middleware/request_verification/request_verification.py index cae8315e6..694cb4960 100644 --- a/slack_bolt/middleware/request_verification/request_verification.py +++ b/slack_bolt/middleware/request_verification/request_verification.py @@ -26,6 +26,8 @@ def __init__(self, signing_secret: str, base_logger: Optional[Logger] = None): @property def verifier(self) -> SignatureVerifier: + # Defer initialization to avoid errors of a missing signing secret for + # apps using Socket Mode connections if self._verifier is None: self._verifier = SignatureVerifier(signing_secret=self._signing_secret) return self._verifier From cab09a4d0e08396727301abe11bb0ba6539e268a Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 12:47:30 -0700 Subject: [PATCH 4/5] style: sort typing imports in request_verification Co-Authored-By: Claude --- .../middleware/request_verification/request_verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slack_bolt/middleware/request_verification/request_verification.py b/slack_bolt/middleware/request_verification/request_verification.py index 694cb4960..b9da4a7a3 100644 --- a/slack_bolt/middleware/request_verification/request_verification.py +++ b/slack_bolt/middleware/request_verification/request_verification.py @@ -1,5 +1,5 @@ from logging import Logger -from typing import Callable, Dict, Any, Optional +from typing import Any, Callable, Dict, Optional from slack_sdk.signature import SignatureVerifier From 2ae9fe65b052ed3c578cd02be42f8249edd4a3ad Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Tue, 30 Jun 2026 13:03:55 -0700 Subject: [PATCH 5/5] docs: generalize reasoning for deferred initialization Co-authored-by: William Bergamin --- .../middleware/request_verification/request_verification.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/slack_bolt/middleware/request_verification/request_verification.py b/slack_bolt/middleware/request_verification/request_verification.py index b9da4a7a3..af505bc84 100644 --- a/slack_bolt/middleware/request_verification/request_verification.py +++ b/slack_bolt/middleware/request_verification/request_verification.py @@ -26,8 +26,7 @@ def __init__(self, signing_secret: str, base_logger: Optional[Logger] = None): @property def verifier(self) -> SignatureVerifier: - # Defer initialization to avoid errors of a missing signing secret for - # apps using Socket Mode connections + # Defer initialization to avoid errors during start up if self._verifier is None: self._verifier = SignatureVerifier(signing_secret=self._signing_secret) return self._verifier