@@ -24,28 +24,54 @@ def parse_query(query: Optional[Union[str, Dict[str, str], Dict[str, Sequence[st
2424 raise ValueError (f"Unsupported type of query detected ({ type (query )} )" )
2525
2626
27+ def _parse_json_object (text : str ) -> Dict [str , Any ]:
28+ # Slack always sends a JSON object. Anything else (broken JSON, an array, a number, ...)
29+ # cannot come from Slack, so it is treated as an empty body. This lets the request reach
30+ # the signature check, which then rejects it, instead of failing while being parsed.
31+ try :
32+ parsed = json .loads (text )
33+ except ValueError :
34+ return {}
35+ return parsed if isinstance (parsed , dict ) else {}
36+
37+
2738def parse_body (body : str , content_type : Optional [str ]) -> Dict [str , Any ]:
2839 if not body :
2940 return {}
3041 if (content_type is not None and content_type == "application/json" ) or body .startswith ("{" ):
31- return json . loads (body )
42+ return _parse_json_object (body )
3243 else :
3344 if "payload" in body : # This is not JSON format yet
3445 params = dict (parse_qsl (body , keep_blank_values = True ))
3546 payload = params .get ("payload" )
3647 if payload is not None :
37- return json . loads (payload )
48+ return _parse_json_object (payload )
3849 else :
3950 return {}
4051 else :
4152 return dict (parse_qsl (body , keep_blank_values = True ))
4253
4354
55+ def _first_authorization (payload : Dict [str , Any ]) -> Optional [Dict [str , Any ]]:
56+ # Returns payload["authorizations"][0] only when it really is a dict
57+ authorizations = payload .get ("authorizations" )
58+ if isinstance (authorizations , list ) and len (authorizations ) > 0 and isinstance (authorizations [0 ], dict ):
59+ return authorizations [0 ]
60+ return None
61+
62+
63+ def _event (payload : Dict [str , Any ]) -> Dict [str , Any ]:
64+ # Returns payload["event"] when it is a dict, otherwise an empty dict
65+ event = payload .get ("event" )
66+ return event if isinstance (event , dict ) else {}
67+
68+
4469def extract_is_enterprise_install (payload : Dict [str , Any ]) -> Optional [bool ]:
45- if payload .get ("authorizations" ) is not None and len (payload ["authorizations" ]) > 0 :
70+ authorization = _first_authorization (payload )
71+ if authorization is not None :
4672 # To make Events API handling functioning also for shared channels,
4773 # we should use .authorizations[0].is_enterprise_install over .is_enterprise_install
48- return extract_is_enterprise_install (payload [ "authorizations" ][ 0 ] )
74+ return extract_is_enterprise_install (authorization )
4975 if "is_enterprise_install" in payload :
5076 is_enterprise_install = payload .get ("is_enterprise_install" )
5177 return is_enterprise_install is not None and (is_enterprise_install is True or is_enterprise_install == "true" )
@@ -57,12 +83,13 @@ def extract_enterprise_id(payload: Dict[str, Any]) -> Optional[str]:
5783 if org is not None :
5884 if isinstance (org , str ):
5985 return org
60- elif "id" in org :
86+ elif isinstance ( org , dict ) and "id" in org :
6187 return org .get ("id" )
62- if payload .get ("authorizations" ) is not None and len (payload ["authorizations" ]) > 0 :
88+ authorization = _first_authorization (payload )
89+ if authorization is not None :
6390 # To make Events API handling functioning also for shared channels,
6491 # we should use .authorizations[0].enterprise_id over .enterprise_id
65- return extract_enterprise_id (payload [ "authorizations" ][ 0 ] )
92+ return extract_enterprise_id (authorization )
6693 if "enterprise_id" in payload :
6794 return payload .get ("enterprise_id" )
6895 if isinstance (payload .get ("team" ), dict ) and "enterprise_id" in payload ["team" ]:
@@ -78,7 +105,7 @@ def extract_actor_enterprise_id(payload: Dict[str, Any]) -> Optional[str]:
78105 if payload .get ("type" ) == "event_callback" :
79106 # For safety, we don't set actor IDs for the events like "file_shared",
80107 # which do not provide any team ID in $.event data. In the case, the IDs cannot be correct.
81- event_team_id = payload . get ( "event" , {} ).get ("user_team" ) or payload . get ( "event" , {} ).get ("team" )
108+ event_team_id = _event ( payload ).get ("user_team" ) or _event ( payload ).get ("team" )
82109 if event_team_id is not None and str (event_team_id ).startswith ("E" ):
83110 return event_team_id
84111 if event_team_id == payload .get ("team_id" ):
@@ -101,12 +128,13 @@ def extract_team_id(payload: Dict[str, Any]) -> Optional[str]:
101128 team = payload .get ("team" )
102129 if isinstance (team , str ):
103130 return team
104- elif team and "id" in team :
131+ elif isinstance ( team , dict ) and "id" in team :
105132 return team .get ("id" )
106- if payload .get ("authorizations" ) is not None and len (payload ["authorizations" ]) > 0 :
133+ authorization = _first_authorization (payload )
134+ if authorization is not None :
107135 # To make Events API handling functioning also for shared channels,
108136 # we should use .authorizations[0].team_id over .team_id
109- return extract_team_id (payload [ "authorizations" ][ 0 ] )
137+ return extract_team_id (authorization )
110138 if "team_id" in payload :
111139 return payload .get ("team_id" )
112140 if isinstance (payload .get ("event" ), dict ):
@@ -121,22 +149,23 @@ def extract_team_id(payload: Dict[str, Any]) -> Optional[str]:
121149def extract_actor_team_id (payload : Dict [str , Any ]) -> Optional [str ]:
122150 if payload .get ("is_ext_shared_channel" ) is True :
123151 if payload .get ("type" ) == "event_callback" :
124- event_type = payload .get ("event" , {}).get ("type" )
152+ event = _event (payload )
153+ event_type = event .get ("type" )
125154 if event_type == "app_mention" :
126155 # The $.event.user_team can be an enterprise_id in app_mention events.
127156 # In the scenario, there is no way to retrieve actor_team_id as of March 2023
128- user_team = payload . get ( " event" , {}) .get ("user_team" )
157+ user_team = event .get ("user_team" )
129158 if user_team is None :
130159 # working with an app installed in this user's org/workspace side
131- return payload . get ( " event" , {}) .get ("team" )
160+ return event .get ("team" )
132161 if str (user_team ).startswith ("T" ):
133162 # interacting from a connected non-grid workspace
134163 return user_team
135164 # Interacting from a connected grid workspace; in this case, team_id cannot be resolved as of March 2023
136165 return None
137166 # For safety, we don't set actor IDs for the events like "file_shared",
138167 # which do not provide any team ID in $.event data. In the case, the IDs cannot be correct.
139- event_user_team = payload . get ( " event" , {}) .get ("user_team" )
168+ event_user_team = event .get ("user_team" )
140169 if event_user_team is not None :
141170 if str (event_user_team ).startswith ("T" ):
142171 return event_user_team
@@ -146,7 +175,7 @@ def extract_actor_team_id(payload: Dict[str, Any]) -> Optional[str]:
146175 elif event_user_team == payload .get ("context_enterprise_id" ):
147176 return payload .get ("context_team_id" )
148177
149- event_team = payload . get ( " event" , {}) .get ("team" )
178+ event_team = event .get ("team" )
150179 if event_team is not None :
151180 if str (event_team ).startswith ("T" ):
152181 return event_team
@@ -165,7 +194,7 @@ def extract_user_id(payload: Dict[str, Any]) -> Optional[str]:
165194 if user is not None :
166195 if isinstance (user , str ):
167196 return user
168- elif "id" in user :
197+ elif isinstance ( user , dict ) and "id" in user :
169198 return user .get ("id" )
170199 if "user_id" in payload :
171200 return payload .get ("user_id" )
@@ -184,7 +213,7 @@ def extract_actor_user_id(payload: Dict[str, Any]) -> Optional[str]:
184213 if payload .get ("is_ext_shared_channel" ) is True :
185214 if payload .get ("type" ) == "event_callback" :
186215 event = payload .get ("event" )
187- if event is None :
216+ if not isinstance ( event , dict ) :
188217 return None
189218 if extract_actor_enterprise_id (payload ) is None and extract_actor_team_id (payload ) is None :
190219 # When both enterprise_id and team_id are not identified, we skip returning user_id too for safety
@@ -198,7 +227,7 @@ def extract_channel_id(payload: Dict[str, Any]) -> Optional[str]:
198227 if channel is not None :
199228 if isinstance (channel , str ):
200229 return channel
201- elif "id" in channel :
230+ elif isinstance ( channel , dict ) and "id" in channel :
202231 return channel .get ("id" )
203232 if "channel_id" in payload :
204233 return payload .get ("channel_id" )
@@ -295,7 +324,7 @@ def build_context(context: BoltContext, body: Dict[str, Any]) -> BoltContext:
295324 elif "response_urls" in body :
296325 # In the case where response_url_enabled: true in a modal exists
297326 response_urls = body ["response_urls" ]
298- if len (response_urls ) >= 1 :
327+ if isinstance ( response_urls , list ) and len (response_urls ) >= 1 and isinstance ( response_urls [ 0 ], dict ) :
299328 if len (response_urls ) > 1 :
300329 context .logger .debug (debug_multiple_response_urls_detected ())
301330 response_url = response_urls [0 ].get ("response_url" )
0 commit comments