|
21 | 21 | import contextlib
|
22 | 22 | import logging
|
23 | 23 | import time
|
| 24 | +from http import HTTPStatus |
24 | 25 | from typing import TYPE_CHECKING, Any, Generator, Optional, Tuple, Union
|
25 | 26 |
|
26 | 27 | import attr
|
@@ -139,6 +140,41 @@ def __repr__(self) -> str:
|
139 | 140 | self.synapse_site.site_tag,
|
140 | 141 | )
|
141 | 142 |
|
| 143 | + # Twisted machinery: this method is called by the Channel once the full request has |
| 144 | + # been received, to dispatch the request to a resource. |
| 145 | + # |
| 146 | + # We're patching Twisted to bail/abort early when we see someone trying to upload |
| 147 | + # `multipart/form-data` so we can avoid Twisted parsing the entire request body into |
| 148 | + # in-memory (specific problem of this specific `Content-Type`). This protects us |
| 149 | + # from an attacker uploading something bigger than the available RAM and crashing |
| 150 | + # the server with a `MemoryError`, or carefully block just enough resources to cause |
| 151 | + # all other requests to fail. |
| 152 | + # |
| 153 | + # FIXME: This can be removed once we Twisted releases a fix and we update to a |
| 154 | + # version that is patched |
| 155 | + def requestReceived(self, command: bytes, path: bytes, version: bytes) -> None: |
| 156 | + if command == b"POST": |
| 157 | + ctype = self.requestHeaders.getRawHeaders(b"content-type") |
| 158 | + if ctype and b"multipart/form-data" in ctype[0]: |
| 159 | + self.method, self.uri = command, path |
| 160 | + self.clientproto = version |
| 161 | + self.code = HTTPStatus.UNSUPPORTED_MEDIA_TYPE.value |
| 162 | + self.code_message = bytes( |
| 163 | + HTTPStatus.UNSUPPORTED_MEDIA_TYPE.phrase, "ascii" |
| 164 | + ) |
| 165 | + self.responseHeaders.setRawHeaders(b"content-length", [b"0"]) |
| 166 | + |
| 167 | + logger.warning( |
| 168 | + "Aborting connection from %s because `content-type: multipart/form-data` is unsupported: %s %s", |
| 169 | + self.client, |
| 170 | + command, |
| 171 | + path, |
| 172 | + ) |
| 173 | + self.write(b"") |
| 174 | + self.loseConnection() |
| 175 | + return |
| 176 | + return super().requestReceived(command, path, version) |
| 177 | + |
142 | 178 | def handleContentChunk(self, data: bytes) -> None:
|
143 | 179 | # we should have a `content` by now.
|
144 | 180 | assert self.content, "handleContentChunk() called before gotLength()"
|
|
0 commit comments