8000 Check if `default_payload` is a dict before using it as a dict by H-Shay · Pull Request #292 · matrix-org/sygnal · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Check if default_payload is a dict before using it as a dict #292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jan 25, 2022
1 change: 1 addition & 0 deletions changelog.d/292.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug introduced in Sygnal 0.7.0 where a malformed `default_payload` could cause an internal server error.
19 changes: 15 additions & 4 deletions sygnal/apnspushkin.py
Original file line number Diff line number Diff line change
8000 Expand Up @@ -283,10 +283,22 @@ async def _dispatch_notification_unlimited(
with self.sygnal.tracer.start_span(
"apns_dispatch", tags=span_tags, child_of=context.opentracing_span
) as span_parent:
# Before we build the payload, check that the default_payload is not
# malformed and reject the pushkey if it is

default_payload = {}

if device.data:
default_payload = device.data.get("default_payload", {})
if not isinstance(default_payload, dict):
log.error(
"default_payload is malformed, this value must be a dict."
)
return [device.pushkey]

if n.event_id and not n.type:
payload: Optional[Dict[str, Any]] = self._get_payload_event_id_only(
n, device
n, default_payload
)
else:
payload = self._get_payload_full(n, device, log)
Expand Down Expand Up @@ -336,7 +348,7 @@ async def _dispatch_notification_unlimited(
raise NotificationDispatchException("Retried too many times.")

def _get_payload_event_id_only(
self, n: Notification, device: Device
self, n: Notification, default_payload: Dict[str, Any]
) -> Dict[str, Any]:
"""
Constructs a payload for a notification where we know only the event ID.
Expand All @@ -350,8 +362,7 @@ def _get_payload_event_id_only(
"""
payload = {}

if device.data:
payload.update(device.data.get("default_payload", {}))
payload.update(default_payload)

if n.room_id:
payload["room_id"] = n.room_id
Expand Down
26 changes: 19 additions & 7 deletions sygnal/gcmpushkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import logging
import time
from io import BytesIO
from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Tuple
from typing import TYPE_CHECKING, Any, AnyStr, Dict, List, Optional, Tuple

from opentracing import Span, logs, tags
from prometheus_client import Counter, Gauge, Histogram
Expand Down Expand Up @@ -347,16 +347,21 @@ async def _dispatch_notification_unlimited(
with self.sygnal.tracer.start_span(
"gcm_dispatch", tags=span_tags, child_of=context.opentracing_span
) as span_parent:
# TODO: Implement collapse_key to queue only one message per room.
failed: List[str] = []

data = GcmPushkin._build_data(n, device)

# Reject pushkey if default_payload is misconfigured
if data is None:
failed.append(device.pushkey)
Comment on lines +356 to +357
Copy link
Member

Choose a reason for hiding this comment

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

looks like this will go on to attempt delivery anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It sure does look like that-which probably doesn't make sense. I can fix that along with downgrading the error to a warning.


headers = {
"User-Agent": ["sygnal"],
"Content-Type": ["application/json"],
"Authorization": ["key=%s" % (self.api_key,)],
}

# TODO: Implement collapse_key to queue only one message per room.
failed: List[str] = []

body = self.base_request_body.copy()
body["data"] = data
body["priority"] = "normal" if n.prio == "low" else "high"
Expand Down Expand Up @@ -409,7 +414,7 @@ async def _dispatch_notification_unlimited(
return failed

@staticmethod
def _build_data(n: Notification, device: Device) -> Dict[str, Any]:
def _build_data(n: Notification, device: Device) -> Optional[Dict[str, Any]]:
"""
Build the payload data to be sent.
Args:
Expand All @@ -418,12 +423,19 @@ def _build_data(n: Notification, device: Device) -> Dict[str, Any]:
will be sent.

Returns:
JSON-compatible dict
JSON-compatible dict or None if the default_payload is misconfigured
"""
data = {}

if device.data:
data.update(device.data.get("default_payload", {}))
default_payload = device.data.get("default_payload", {})
if isinstance(default_payload, dict):
data.update(default_payload)
else:
logger.error(
"default_payload was misconfigured, this value must be a dict."
)
return None

for attr in [
"event_id",
Expand Down
16 changes: 16 additions & 0 deletions tests/test_apns.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
},
}

DEVICE_EXAMPLE_WITH_BAD_DEFAULT_PAYLOAD = {
"app_id": "com.example.apns",
"pushkey": "badpayload",
"pushkey_ts": 42,
"data": {"default_payload": None},
}


class ApnsTestCase(testutils.TestCase):
def setUp(self):
Expand Down Expand Up @@ -264,6 +271,15 @@ def test_expected_full_with_default_payload(self):

self.assertEqual({"rejected": []}, resp)

def test_misconfigured_payload_is_rejected(self):
"""Test that a malformed default_payload causes pushkey to be rejected"""

resp = self._request(
self._make_dummy_notification([DEVICE_EXAMPLE_WITH_BAD_DEFAULT_PAYLOAD])
)

self.assertEqual({"rejected": ["badpayload"]}, resp)

def test_rejection(self):
"""
Tests the rejection case: a rejection response from APNS leads to us
Expand Down
6D40
26 changes: 26 additions & 0 deletions tests/test_gcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
}
},
}

DEVICE_EXAMPLE_WITH_BAD_DEFAULT_PAYLOAD = {
"app_id": "com.example.gcm",
"pushkey": "badpayload",
"pushkey_ts": 42,
"data": {
"default_payload": None,
},
}

DEVICE_EXAMPLE_IOS = {
"app_id": "com.example.gcm.ios",
"pushkey": "spqr",
Expand Down Expand Up @@ -118,6 +128,22 @@ def test_expected_with_default_payload(self):
self.assertEqual(resp, {"rejected": []})
self.assertEqual(gcm.num_requests, 1)

def test_misformed_default_payload_rejected(self):
"""
Tests that a non-dict default_payload is rejected.
"""
gcm = self.get_test_pushkin("com.example.gcm")
gcm.preload_with_response(
200, {"results": [{"message_id": "msg42", "registration_id": "badpayload"}]}
)

resp = self._request(
self._make_dummy_notification([DEVICE_EXAMPLE_WITH_BAD_DEFAULT_PAYLOAD])
)

self.assertEqual(resp, {"rejected": ["badpayload"]})
self.assertEqual(gcm.num_requests, 1)

def test_rejected(self):
"""
Tests the rejected case: a pushkey rejected to GCM leads to Sygnal
Expand Down
0