Repository navigation
extract_team_id crashes with TypeError when payload["user"] is a string (e.g. member_joined_channel events) #1447
Description
Activity
- changed the title
[-]extract_team_id crashes with TypeError when payload["user"] is a string (e.g. member_joined_channel events)[/-][+]`extract_team_id` crashes with `TypeError when payload["user"]` is a string (e.g. member_joined_channel events)[/+]on Feb 22, 2026 - changed the title
[-]`extract_team_id` crashes with `TypeError when payload["user"]` is a string (e.g. member_joined_channel events)[/-][+]`extract_team_id` crashes with `TypeError when payload["user"]` is a string (e.g. `member_joined_channel` events)[/+]on Feb 22, 2026 Hi @dbermuehler 👋 thanks for writing in!
I tried to reproduce this behavior with the following app
import os import logging from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler logging.basicConfig(level=logging.DEBUG) app = App(token=os.environ.get("SLACK_BOT_TOKEN")) @app.event("member_joined_channel") def handle_channel_join(event, client, say): say("Hello!") if __name__ == "__main__": SocketModeHandler(app, os.environ.get("SLACK_APP_TOKEN")).start()
manifest.json
{ "_metadata": { "major_version": 1, "minor_version": 1 }, "display_information": { "name": "member_joined_channel" }, "features": { "bot_user": { "display_name": "member_joined_channel", "always_online": false } }, "oauth_config": { "scopes": { "bot": [ "channels:read", "chat:write" ] } }, "settings": { "event_subscriptions": { "bot_events": [ "member_joined_channel" ] }, "org_deploy_enabled": true, "socket_mode_enabled": true, "token_rotation_enabled": false } }I was not able to reproduce your behavior, the error you shared in your description was not encountered in my testing 🤔
Example payload
{ "token": "q123", "team_id": "T123", "context_team_id": "T123", "context_enterprise_id": null, "api_app_id": "A123", "event": { "type": "member_joined_channel", "user": "U123", "channel": "C123", "channel_type": "C", "team": "T123", "inviter": "U123", "event_ts": "123" }, "type": "event_callback", "event_id": "E123", "event_time": 123, "authorizations": [ { "enterprise_id": null, "team_id": "T123, "user_id": "U123", "is_bot": true, "is_enterprise_install": false } ], "is_ext_shared_channel": false, "event_context": "4-123" },The current implementation of extract_team_id will go through the following flow
- Finds that
eventis in thepayload - Recessively call
extract_team_id(payload["event"]) - Find that
teamis in the newpayloadand that it is astr - return that value
Since
teamis always in thisevent, theteam_idvalue will always be returned beforepayload['event']['user']is assumed to be adictIf there are different testing steps I could follow to reproduce your behavior, please share them with me
- Finds that
- addedquestionFurther information is requestedFurther information is requestedand removedquestionFurther information is requestedFurther information is requested
on Feb 23, 2026 Thanks for looking into this @WilliamBergamin — you're right,
teamis always present in real Slackmember_joined_channelevents, soextract_team_idreturns before reachingpayload["user"].After investigating further, we believe the crash was caused by non-Slack traffic (scrapers/bots) hitting our public API Gateway endpoint with malformed payloads that were missing the
teamfield. We're running the Bolt app in an AWS Lambda function behind API Gateway usingSlackRequestHandler, so the endpoint is publicly reachable. Unfortunately we don't have logs of the raw request body, so we can't confirm the exact payload that caused the crash.We expected
RequestVerificationto reject these unsigned requests, but the crash happens before signature verification runs.The issue is an ordering problem in the AWS Lambda adapter. Here's the full chain:
handler.py#L55—handle()callsto_bolt_request(event)handler.py#L87-L91— constructsBoltRequest(body=body, ...)request.py#L63—BoltRequest.__init__()parses the bodyrequest.py#L69— callsbuild_context()on the parsed bodyinternals.py#L276—build_context()callsextract_team_id(body)internals.py#L115-L116— crashes onpayload["user"]["team_id"]whenuseris a string
The
BoltRequestconstruction fails, so execution never reaches:handler.py#L61—app.dispatch(bolt_req)app.py#L559-L563— iterates_middleware_listapp.py#L411—RequestVerificationis registered in that listrequest_verification.py#L42— signature would be checked here
The crash at step 6 prevents the signature check at step 10 from ever running. We worked around this by verifying the Slack signature ourselves before calling into slack_bolt, but it might be worth considering moving signature verification ahead of body parsing in the adapter layer itself.
Reacted by William BergaminWe managed to reproduce this. Here's a minimal PoC using the AWS Lambda adapter with real credentials and
RequestVerificationenabled (the default):"""PoC: Malformed request crashes slack_bolt before signature verification runs. Simulates a scraper sending a crafted payload to a Bolt app behind AWS API Gateway. The payload has "user" as a string but no "team" field, causing extract_team_id() to crash during BoltRequest construction — before RequestVerification middleware ever gets a chance to reject the unsigned request. """ import json from slack_bolt import App from slack_bolt.adapter.aws_lambda.handler import SlackRequestHandler app = App(signing_secret="your-signing-secret", token="xoxb-your-bot-token") @app.event("member_joined_channel") def handle_channel_join(event, say): say("Hello!") handler = SlackRequestHandler(app) # Malformed payload: "user" is a string, "team" is missing. # A real Slack payload always includes "team", but a scraper doesn't know that. malformed_event = { "requestContext": {"http": {"method": "POST"}}, "headers": {}, "body": json.dumps( { "type": "event_callback", "event": { "type": "member_joined_channel", "user": "U0123456789", "channel": "C0123456789", }, } ), "isBase64Encoded": False, } class FakeLambdaContext: function_name = "test" invoked_function_arn = "arn:aws:lambda:eu-west-1:123:function:test" try: response = handler.handle(malformed_event, FakeLambdaContext()) print(f"Response: {response}") except TypeError as e: print(f"CRASHED with TypeError: {e}")
Output:
CRASHED with TypeError: string indices must be integers, not 'str'Note that
RequestVerificationis enabled — the app is configured with a real signing secret. The request has no valid signature, so it should be rejected with 401. Instead it crashes becauseBoltRequest.__init__()parses the body and callsextract_team_id()beforedispatch()runs theRequestVerificationmiddleware.@WilliamBergamin just wanted to follow up on this: Since this seems like a different issue than the one for which I originally created the issue, should we close this one and create a new issue?
- addedbugSomething isn't workingSomething isn't workingand removed
on Mar 18, 2026 This does seems like an a potential edge case issue 🤔 Its an edge case since this error would only show up if some rogue request is able to reach your application
The ideal solution would be to move the context initialization into a middleware that runs after the
RequestVerificationmiddleware⚠️ But this would constitute a breaking change and would require a major releaseAs a temporary fix we might be able to improve the robustness of our
build_contextlogic so that it is less likely to cause issues before reaching theRequestVerificationmiddleware.This does seems like an a potential edge case issue 🤔 Its an edge case since this error would only show up if some rogue request is able to reach your application
I would say that this is not a very uncommon edge case. Scanners that crawl the internet for vulnerable endpoints which will send invalid requests are unfortunately a normality in today's internet. A potential solution would be to only allow Slack IP addresses access to the endpoint and block anything else, however, that is not really practical since the addresses are changing constantly and keeping them up to date would be a hassle.
Also from a security perspective I think it is cleaner and more secure to do input validation as early as possible rather than deep in the code.
But I totally understand that this might require a bigger change and a major version bump. Looking forward in seeing this change!
Lets keep this open for now, since what we implemented is a patch fix to the root cause
👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.
🌚 Keeping open as we track upcoming release 1.29.0 in milestones!
- added a commit that references this issue
on Oct 8, 2026
Reproducible in:
The
slack_boltversionPython runtime version
OS info
Steps to reproduce:
member_joined_channellistener in a Slack Bolt app:Add the bot to any Slack channel.
Observe the incoming event payload — Slack sends
"user"as a plain string (the user ID):{ "type": "member_joined_channel", "user": "U0123456789", "channel": "C0123456789", "team": "T0123456789" }Expected result:
The listener is invoked normally.
Actual result:
The request crashes before the listener is reached, with:
Root cause
extract_team_idinslack_bolt/request/internals.pyassumespayload["user"]is always a dict:But
member_joined_channelsends"user"as a plain string (the user ID). Accessing"U0123456789"["team_id"]raises the TypeError.The same function already handles
payload["team"]correctly with anisinstancecheck:The same guard is missing for
payload["user"]. This pattern (subscribing tomember_joined_channeland checking if the joined user is the bot) is what maintainers recommend in #710, so users following that guidance will hit this bug.