Skip to content

Commit 2393342

Browse files
Eeshan Gargtimabbott
Eeshan Garg
authored andcommitted
webhooks/jira: Handle anomalous payloads properly.
We recently ran into a payload in production that didn't contain an event type at all. A payload where we can't figure out the event type is quite rare. Instead of letting these payloads run amok, we should raise a more informative exception for such unusual payloads. If we encounter too many of these, then we can choose to conduct a deeper investigation on a case-by-case basis. With some changes by Tim Abbott.
1 parent c5c3ab6 commit 2393342

File tree

8 files changed

+110
-32
lines changed

8 files changed

+110
-32
lines changed

zerver/decorator.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from zerver.lib.cache import cache_with_key
2929
from zerver.lib.exceptions import (
3030
AccessDeniedError,
31+
AnomalousWebhookPayload,
3132
ErrorCode,
3233
InvalidAPIKeyError,
3334
InvalidAPIKeyFormatError,
@@ -40,6 +41,7 @@
4041
RealmDeactivatedError,
4142
UnsupportedWebhookEventType,
4243
UserDeactivatedError,
44+
WebhookError,
4345
)
4446
from zerver.lib.queue import queue_json_publish
4547
from zerver.lib.rate_limiter import RateLimitedIPAddr, RateLimitedUser
@@ -62,6 +64,7 @@
6264

6365
webhook_logger = logging.getLogger("zulip.zerver.webhooks")
6466
webhook_unsupported_events_logger = logging.getLogger("zulip.zerver.webhooks.unsupported")
67+
webhook_anomalous_payloads_logger = logging.getLogger("zulip.zerver.webhooks.anomalous")
6568

6669
FuncT = TypeVar("FuncT", bound=Callable[..., object])
6770

@@ -305,14 +308,23 @@ def access_user_by_api_key(
305308
return user_profile
306309

307310

308-
def log_exception_to_webhook_logger(
309-
summary: str,
310-
unsupported_event: bool,
311-
) -> None:
312-
if unsupported_event:
313-
webhook_unsupported_events_logger.exception(summary, stack_info=True)
311+
def log_unsupported_webhook_event(summary: str) -> None:
312+
# This helper is primarily used by some of our more complicated
313+
# webhook integrations (e.g. GitHub) that need to log an unsupported
314+
# event based on attributes nested deep within a complicated JSON
315+
# payload. In such cases, the error message we want to log may not
316+
# really fit what a regular UnsupportedWebhookEventType exception
317+
# represents.
318+
webhook_unsupported_events_logger.exception(summary, stack_info=True)
319+
320+
321+
def log_exception_to_webhook_logger(err: Exception) -> None:
322+
if isinstance(err, AnomalousWebhookPayload):
323+
webhook_anomalous_payloads_logger.exception(str(err), stack_info=True)
324+
elif isinstance(err, UnsupportedWebhookEventType):
325+
webhook_unsupported_events_logger.exception(str(err), stack_info=True)
314326
else:
315-
webhook_logger.exception(summary, stack_info=True)
327+
webhook_logger.exception(str(err), stack_info=True)
316328

317329

318330
def full_webhook_client_name(raw_client_name: Optional[str] = None) -> Optional[str]:
@@ -357,17 +369,12 @@ def _wrapped_func_arguments(
357369
from zerver.lib.webhooks.common import notify_bot_owner_about_invalid_json
358370

359371
notify_bot_owner_about_invalid_json(user_profile, webhook_client_name)
360-
elif isinstance(err, JsonableError) and not isinstance(
361-
err, UnsupportedWebhookEventType
362-
):
372+
elif isinstance(err, JsonableError) and not isinstance(err, WebhookError):
363373
pass
364374
else:
365-
if isinstance(err, UnsupportedWebhookEventType):
375+
if isinstance(err, WebhookError):
366376
err.webhook_name = webhook_client_name
367-
log_exception_to_webhook_logger(
368-
summary=str(err),
369-
unsupported_event=isinstance(err, UnsupportedWebhookEventType),
370-
)
377+
log_exception_to_webhook_logger(err)
371378
raise err
372379

373380
_wrapped_func_arguments._all_event_types = all_event_types
@@ -693,16 +700,13 @@ def _wrapped_func_arguments(
693700
if not webhook_client_name:
694701
raise err
695702
if isinstance(err, JsonableError) and not isinstance(
696-
err, UnsupportedWebhookEventType
703+
err, WebhookError
697704
): # nocoverage
698705
raise err
699706

700-
if isinstance(err, UnsupportedWebhookEventType):
707+
if isinstance(err, WebhookError):
701708
err.webhook_name = webhook_client_name
702-
log_exception_to_webhook_logger(
703-
summary=str(err),
704-
unsupported_event=isinstance(err, UnsupportedWebhookEventType),
705-
)
709+
log_exception_to_webhook_logger(err)
706710
raise err
707711

708712
return _wrapped_func_arguments

zerver/lib/exceptions.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class ErrorCode(Enum):
1818
STREAM_DOES_NOT_EXIST = auto()
1919
UNAUTHORIZED_PRINCIPAL = auto()
2020
UNSUPPORTED_WEBHOOK_EVENT_TYPE = auto()
21+
ANOMALOUS_WEBHOOK_PAYLOAD = auto()
2122
BAD_EVENT_QUEUE_ID = auto()
2223
CSRF_FAILED = auto()
2324
INVITATION_FAILED = auto()
@@ -317,19 +318,61 @@ def msg_format() -> str:
317318
return _("Malformed API key")
318319

319320

320-
class UnsupportedWebhookEventType(JsonableError):
321+
class WebhookError(JsonableError):
322+
"""
323+
Intended as a generic exception raised by specific webhook
324+
integrations. This class is subclassed by more specific exceptions
325+
such as UnsupportedWebhookEventType and AnomalousWebhookPayload.
326+
"""
327+
328+
data_fields = ["webhook_name"]
329+
330+
def __init__(self) -> None:
331+
# webhook_name is often set by decorators such as webhook_view
332+
# in zerver/decorator.py
333+
self.webhook_name = "(unknown)"
334+
335+
336+
class UnsupportedWebhookEventType(WebhookError):
337+
"""Intended as an exception for event formats that we know the
338+
third-party service generates but which Zulip doesn't support /
339+
generate a message for.
340+
341+
Exceptions where we cannot parse the event type, possibly because
342+
the event isn't actually from the service in question, should
343+
raise AnomalousWebhookPayload.
344+
"""
345+
321346
code = ErrorCode.UNSUPPORTED_WEBHOOK_EVENT_TYPE
322347
data_fields = ["webhook_name", "event_type"]
323348

324349
def __init__(self, event_type: Optional[str]) -> None:
325-
self.webhook_name = "(unknown)"
350+
super().__init__()
326351
self.event_type = event_type
327352

328353
@staticmethod
329354
def msg_format() -> str:
330355
return _("The '{event_type}' event isn't currently supported by the {webhook_name} webhook")
331356

332357

358+
class AnomalousWebhookPayload(WebhookError):
359+
"""Intended as an exception for incoming webhook requests that we
360+
cannot recognize as having been generated by the service in
361+
question. (E.g. because someone pointed a Jira server at the
362+
GitHub integration URL).
363+
364+
If we can parse the event but don't support it, use
365+
UnsupportedWebhookEventType.
366+
367+
"""
368+
369+
code = ErrorCode.ANOMALOUS_WEBHOOK_PAYLOAD
370+
371+
@staticmethod
372+
def msg_format() -> str:
373+
return _("Unable to parse request: Did {webhook_name} generate this event?")
374+
375+
333376
class MissingAuthenticationError(JsonableError):
334377
code = ErrorCode.UNAUTHENTICATED_USER
335378
http_status_code = 401

zerver/webhooks/bitbucket2/view.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from django.http import HttpRequest, HttpResponse
99

10-
from zerver.decorator import log_exception_to_webhook_logger, webhook_view
10+
from zerver.decorator import log_unsupported_webhook_event, webhook_view
1111
from zerver.lib.exceptions import UnsupportedWebhookEventType
1212
from zerver.lib.request import REQ, has_request_variables
1313
from zerver.lib.response import json_success
@@ -477,11 +477,10 @@ def get_user_info(dct: Dict[str, Any]) -> str:
477477
if "nickname" in dct:
478478
return dct["nickname"]
479479

480-
log_exception_to_webhook_logger(
480+
# We call this an unsupported_event, even though we
481+
# are technically still sending a message.
482+
log_unsupported_webhook_event(
481483
summary="Could not find display_name/nickname field",
482-
# We call this an unsupported_event, even though we
483-
# are technically still sending a message.
484-
unsupported_event=True,
485484
)
486485

487486
return "Unknown user"

zerver/webhooks/github/view.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from django.http import HttpRequest, HttpResponse
66

7-
from zerver.decorator import log_exception_to_webhook_logger, webhook_view
7+
from zerver.decorator import log_unsupported_webhook_event, webhook_view
88
from zerver.lib.exceptions import UnsupportedWebhookEventType
99
from zerver.lib.request import REQ, has_request_variables
1010
from zerver.lib.response import json_success
@@ -45,9 +45,8 @@ def __init__(
4545

4646
def log_unsupported(self, event: str) -> None:
4747
summary = f"The '{event}' event isn't currently supported by the GitHub webhook"
48-
log_exception_to_webhook_logger(
48+
log_unsupported_webhook_event(
4949
summary=summary,
50-
unsupported_event=True,
5150
)
5251

5352

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"accountId": "1234asdfjlsweoiruoso",
3+
"username": "eeshangarg"
4+
}

zerver/webhooks/jira/tests.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,17 @@ def test_comment_event_comment_deleted(self) -> None:
234234
expected_topic = "SP-1: Add support for newer format Jira issue comment events"
235235
expected_message = """Hemanth V. Alluri deleted their comment on issue: *"Add support for newer format Jira issue comment events"*\n``` quote\n~~This is a very important issue! I’m on it!~~\n```"""
236236
self.check_webhook("comment_deleted", expected_topic, expected_message)
237+
238+
def test_anomalous_webhook_payload_error(self) -> None:
239+
with self.assertRaises(AssertionError) as e:
240+
self.check_webhook(
241+
fixture_name="example_anomalous_payload",
242+
expected_topic="",
243+
expected_message="",
244+
expect_noop=True,
245+
)
246+
247+
self.assertIn(
248+
"Unable to parse request: Did Jira generate this event?",
249+
e.exception.args[0],
250+
)

zerver/webhooks/jira/view.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.http import HttpRequest, HttpResponse
88

99
from zerver.decorator import webhook_view
10-
from zerver.lib.exceptions import UnsupportedWebhookEventType
10+
from zerver.lib.exceptions import AnomalousWebhookPayload, UnsupportedWebhookEventType
1111
from zerver.lib.request import REQ, has_request_variables
1212
from zerver.lib.response import json_success
1313
from zerver.lib.webhooks.common import check_send_webhook_message
@@ -349,6 +349,9 @@ def api_jira_webhook(
349349
if event in IGNORED_EVENTS:
350350
return json_success()
351351

352+
if event is None:
353+
raise AnomalousWebhookPayload()
354+
352355
if event is not None:
353356
content_func = JIRA_CONTENT_FUNCTION_MAPPER.get(event)
354357

zproject/computed_settings.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,7 @@ def zulip_path(path: str) -> str:
717717
ANALYTICS_LOG_PATH = zulip_path("/var/log/zulip/analytics.log")
718718
ANALYTICS_LOCK_DIR = zulip_path("/home/zulip/deployments/analytics-lock-dir")
719719
WEBHOOK_LOG_PATH = zulip_path("/var/log/zulip/webhooks_errors.log")
720+
WEBHOOK_ANOMALOUS_PAYLOADS_LOG_PATH = zulip_path("/var/log/zulip/webhooks_anomalous_payloads.log")
720721
WEBHOOK_UNSUPPORTED_EVENTS_LOG_PATH = zulip_path("/var/log/zulip/webhooks_unsupported_events.log")
721722
SOFT_DEACTIVATION_LOG_PATH = zulip_path("/var/log/zulip/soft_deactivation.log")
722723
TRACEMALLOC_DUMP_DIR = zulip_path("/var/log/zulip/tracemalloc")
@@ -849,6 +850,12 @@ def zulip_path(path: str) -> str:
849850
"formatter": "webhook_request_data",
850851
"filename": WEBHOOK_UNSUPPORTED_EVENTS_LOG_PATH,
851852
},
853+
"webhook_anomalous_file": {
854+
"level": "DEBUG",
855+
"class": "logging.handlers.WatchedFileHandler",
856+
"formatter": "webhook_request_data",
857+
"filename": WEBHOOK_ANOMALOUS_PAYLOADS_LOG_PATH,
858+
},
852859
},
853860
"loggers": {
854861
# The Python logging module uses a hierarchy of logger names for config:
@@ -1003,6 +1010,11 @@ def zulip_path(path: str) -> str:
10031010
"handlers": ["webhook_unsupported_file"],
10041011
"propagate": False,
10051012
},
1013+
"zulip.zerver.webhooks.anomalous": {
1014+
"level": "DEBUG",
1015+
"handlers": ["webhook_anomalous_file"],
1016+
"propagate": False,
1017+
},
10061018
},
10071019
}
10081020

0 commit comments

Comments
 (0)