Skip to content

Commit 4b7154c

Browse files
devonhMadLittleMods
authored andcommitted
Don't allow unsupported content-type
Co-authored-by: Eric Eastwood <[email protected]>
1 parent d82e1ed commit 4b7154c

File tree

2 files changed

+89
-0
lines changed

2 files changed

+89
-0
lines changed

synapse/http/site.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import contextlib
2222
import logging
2323
import time
24+
from http import HTTPStatus
2425
from typing import TYPE_CHECKING, Any, Generator, Optional, Tuple, Union
2526

2627
import attr
@@ -139,6 +140,41 @@ def __repr__(self) -> str:
139140
self.synapse_site.site_tag,
140141
)
141142

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+
142178
def handleContentChunk(self, data: bytes) -> None:
143179
# we should have a `content` by now.
144180
assert self.content, "handleContentChunk() called before gotLength()"

tests/http/test_site.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,56 @@ def test_large_request(self) -> None:
9090
# default max upload size is 50M, so it should drop on the next buffer after
9191
# that.
9292
self.assertEqual(sent, 50 * 1024 * 1024 + 1024)
93+
94+
def test_content_type_multipart(self) -> None:
95+
"""HTTP POST requests with `content-type: multipart/form-data` should be rejected"""
96+
self.hs.start_listening()
97+
98+
# find the HTTP server which is configured to listen on port 0
99+
(port, factory, _backlog, interface) = self.reactor.tcpServers[0]
100+
self.assertEqual(interface, "::")
101+
self.assertEqual(port, 0)
102+
103+
# as a control case, first send a regular request.
104+
105+
# complete the connection and wire it up to a fake transport
106+
client_address = IPv6Address("TCP", "::1", 2345)
107+
protocol = factory.buildProtocol(client_address)
108+
transport = StringTransport()
109+
protocol.makeConnection(transport)
110+
111+
protocol.dataReceived(
112+
b"POST / HTTP/1.1\r\n"
113+
b"Connection: close\r\n"
114+
b"Transfer-Encoding: chunked\r\n"
115+
b"\r\n"
116+
b"0\r\n"
117+
b"\r\n"
118+
)
119+
120+
while not transport.disconnecting:
121+
self.reactor.advance(1)
122+
123+
# we should get a 404
124+
self.assertRegex(transport.value().decode(), r"^HTTP/1\.1 404 ")
125+
126+
# now send request with content-type header
127+
protocol = factory.buildProtocol(client_address)
128+
transport = StringTransport()
129+
protocol.makeConnection(transport)
130+
131+
protocol.dataReceived(
132+
b"POST / HTTP/1.1\r\n"
133+
b"Connection: close\r\n"
134+
b"Transfer-Encoding: chunked\r\n"
135+
b"Content-Type: multipart/form-data\r\n"
136+
b"\r\n"
137+
b"0\r\n"
138+
b"\r\n"
139+
)
140+
141+
while not transport.disconnecting:
142+
self.reactor.advance(1)
143+
144+
# we should get a 415
145+
self.assertRegex(transport.value().decode(), r"^HTTP/1\.1 415 ")

0 commit comments

Comments
 (0)