diff --git a/changelog.d/216.bugfix b/changelog.d/216.bugfix new file mode 100644 index 00000000..bf9ad489 --- /dev/null +++ b/changelog.d/216.bugfix @@ -0,0 +1 @@ +Fix a long-standing bug where invalid JSON would be accepted over the HTTP interfaces. diff --git a/sygnal/gcmpushkin.py b/sygnal/gcmpushkin.py index 1967dd37..e3bbbec4 100644 --- a/sygnal/gcmpushkin.py +++ b/sygnal/gcmpushkin.py @@ -18,7 +18,6 @@ import logging import time from io import BytesIO -from json import JSONDecodeError from opentracing import logs, tags from prometheus_client import Counter, Gauge, Histogram @@ -33,7 +32,7 @@ ) from sygnal.helper.context_factory import ClientTLSOptionsFactory from sygnal.helper.proxy.proxyagent_twisted import ProxyAgent -from sygnal.utils import NotificationLoggerAdapter, twisted_sleep +from sygnal.utils import NotificationLoggerAdapter, json_decoder, twisted_sleep from .exceptions import PushkinSetupException from .notifications import ConcurrencyLimitedPushkin @@ -251,8 +250,8 @@ async def _request_dispatch(self, n, log, body, headers, pushkeys, span): return pushkeys, [] elif 200 <= response.code < 300: try: - resp_object = json.loads(response_text) - except JSONDecodeError: + resp_object = json_decoder.decode(response_text) + except ValueError: raise NotificationDispatchException("Invalid JSON response from GCM.") if "results" not in resp_object: log.error( diff --git a/sygnal/http.py b/sygnal/http.py index 7facc7be..5558ecb3 100644 --- a/sygnal/http.py +++ b/sygnal/http.py @@ -35,7 +35,7 @@ from twisted.web.server import NOT_DONE_YET from sygnal.notifications import NotificationContext -from sygnal.utils import NotificationLoggerAdapter +from sygnal.utils import NotificationLoggerAdapter, json_decoder from .exceptions import InvalidNotificationException, NotificationDispatchException from .notifications import Notification @@ -133,7 +133,7 @@ def _handle_request(self, request): log = NotificationLoggerAdapter(logger, {"request_id": request_id}) try: - body = json.loads(request.content.read()) + body = json_decoder.decode(request.content.read().decode("utf-8")) except Exception as exc: msg = "Expected JSON request body" log.warning(msg, exc_info=exc) diff --git a/sygnal/utils.py b/sygnal/utils.py index 3ce3bfaa..fb71e673 100644 --- a/sygnal/utils.py +++ b/sygnal/utils.py @@ -12,6 +12,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import json import re from logging import LoggerAdapter @@ -62,3 +63,12 @@ def glob_to_regex(glob): # \A anchors at start of string, \Z at end of string return re.compile(r"\A" + res + r"\Z", re.IGNORECASE) + + +def _reject_invalid_json(val): + """Do not allow Infinity, -Infinity, or NaN values in JSON.""" + raise ValueError(f"Invalid JSON value: {val!r}") + + +# a custom JSON decoder which will reject Python extensions to JSON. +json_decoder = json.JSONDecoder(parse_constant=_reject_invalid_json)