Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit d939120

Browse files
joshqouclokep
andauthored
Fix unsafe hotserving behaviour for non-multimedia uploads. (#15680)
* Fix unsafe hotserving behaviour for non-multimedia uploads. * invert disposition assert * test_media_storage.py: run lint * test_base.py: /inline/attachment/s * Only return attachment for disposition type, update tests * Update synapse/media/_base.py Co-authored-by: Patrick Cloke <[email protected]> * Update changelog.d/15680.bugfix Co-authored-by: Patrick Cloke <[email protected]> * add attribution * Update changelog. --------- Co-authored-by: Patrick Cloke <[email protected]>
1 parent 1404f68 commit d939120

File tree

4 files changed

+29
-19
lines changed

4 files changed

+29
-19
lines changed

changelog.d/15680.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where media files were served in an unsafe manner. Contributed by @joshqou.

synapse/media/_base.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ def _quote(x: str) -> str:
152152
content_type = media_type
153153

154154
request.setHeader(b"Content-Type", content_type.encode("UTF-8"))
155+
156+
# Use a Content-Disposition of attachment to force download of media.
157+
disposition = "attachment"
155158
if upload_name:
156159
# RFC6266 section 4.1 [1] defines both `filename` and `filename*`.
157160
#
@@ -173,11 +176,17 @@ def _quote(x: str) -> str:
173176
# correctly interpret those as of 0.99.2 and (b) they are a bit of a pain and we
174177
# may as well just do the filename* version.
175178
if _can_encode_filename_as_token(upload_name):
176-
disposition = "inline; filename=%s" % (upload_name,)
179+
disposition = "%s; filename=%s" % (
180+
disposition,
181+
upload_name,
182+
)
177183
else:
178-
disposition = "inline; filename*=utf-8''%s" % (_quote(upload_name),)
184+
disposition = "%s; filename*=utf-8''%s" % (
185+
disposition,
186+
_quote(upload_name),
187+
)
179188

180-
request.setHeader(b"Content-Disposition", disposition.encode("ascii"))
189+
request.setHeader(b"Content-Disposition", disposition.encode("ascii"))
181190

182191
# cache for at least a day.
183192
# XXX: we might want to turn this off for data we don't want to

tests/media/test_base.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@
2020
class GetFileNameFromHeadersTests(unittest.TestCase):
2121
# input -> expected result
2222
TEST_CASES = {
23-
b"inline; filename=abc.txt": "abc.txt",
24-
b'inline; filename="azerty"': "azerty",
25-
b'inline; filename="aze%20rty"': "aze%20rty",
26-
b'inline; filename="aze"rty"': 'aze"rty',
27-
b'inline; filename="azer;ty"': "azer;ty",
28-
b"inline; filename*=utf-8''foo%C2%A3bar": "foo£bar",
23+
b"attachment; filename=abc.txt": "abc.txt",
24+
b'attachment; filename="azerty"': "azerty",
25+
b'attachment; filename="aze%20rty"': "aze%20rty",
26+
b'attachment; filename="aze"rty"': 'aze"rty',
27+
b'attachment; filename="azer;ty"': "azer;ty",
28+
b"attachment; filename*=utf-8''foo%C2%A3bar": "foo£bar",
2929
}
3030

3131
def tests(self) -> None:

tests/media/test_media_storage.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def _req(
317317

318318
def test_handle_missing_content_type(self) -> None:
319319
channel = self._req(
320-
b"inline; filename=out" + self.test_image.extension,
320+
b"attachment; filename=out" + self.test_image.extension,
321321
include_content_type=False,
322322
)
323323
headers = channel.headers
@@ -331,15 +331,15 @@ def test_disposition_filename_ascii(self) -> None:
331331
If the filename is filename=<ascii> then Synapse will decode it as an
332332
ASCII string, and use filename= in the response.
333333
"""
334-
channel = self._req(b"inline; filename=out" + self.test_image.extension)
334+
channel = self._req(b"attachment; filename=out" + self.test_image.extension)
335335

336336
headers = channel.headers
337337
self.assertEqual(
338338
headers.getRawHeaders(b"Content-Type"), [self.test_image.content_type]
339339
)
340340
self.assertEqual(
341341
headers.getRawHeaders(b"Content-Disposition"),
342-
[b"inline; filename=out" + self.test_image.extension],
342+
[b"attachment; filename=out" + self.test_image.extension],
343343
)
344344

345345
def test_disposition_filenamestar_utf8escaped(self) -> None:
@@ -350,7 +350,7 @@ def test_disposition_filenamestar_utf8escaped(self) -> None:
350350
"""
351351
filename = parse.quote("\u2603".encode()).encode("ascii")
352352
channel = self._req(
353-
b"inline; filename*=utf-8''" + filename + self.test_image.extension
353+
b"attachment; filename*=utf-8''" + filename + self.test_image.extension
354354
)
355355

356356
headers = channel.headers
@@ -359,21 +359,21 @@ def test_disposition_filenamestar_utf8escaped(self) -> None:
359359
)
360360
self.assertEqual(
361361
headers.getRawHeaders(b"Content-Disposition"),
362-
[b"inline; filename*=utf-8''" + filename + self.test_image.extension],
362+
[b"attachment; filename*=utf-8''" + filename + self.test_image.extension],
363363
)
364364

365365
def test_disposition_none(self) -> None:
366366
"""
367-
If there is no filename, one isn't passed on in the Content-Disposition
368-
of the request.
367+
If there is no filename, Content-Disposition should only
368+
be a disposition type.
369369
"""
370370
channel = self._req(None)
371371

372372
headers = channel.headers
373373
self.assertEqual(
374374
headers.getRawHeaders(b"Content-Type"), [self.test_image.content_type]
375375
)
376-
self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), None)
376+
self.assertEqual(headers.getRawHeaders(b"Content-Disposition"), [b"attachment"])
377377

378378
def test_thumbnail_crop(self) -> None:
379379
"""Test that a cropped remote thumbnail is available."""
@@ -612,7 +612,7 @@ def test_x_robots_tag_header(self) -> None:
612612
Tests that the `X-Robots-Tag` header is present, which informs web crawlers
613613
to not index, archive, or follow links in media.
614614
"""
615-
channel = self._req(b"inline; filename=out" + self.test_image.extension)
615+
channel = self._req(b"attachment; filename=out" + self.test_image.extension)
616616

617617
headers = channel.headers
618618
self.assertEqual(
@@ -625,7 +625,7 @@ def test_cross_origin_resource_policy_header(self) -> None:
625625
Test that the Cross-Origin-Resource-Policy header is set to "cross-origin"
626626
allowing web clients to embed media from the downloads API.
627627
"""
628-
channel = self._req(b"inline; filename=out" + self.test_image.extension)
628+
channel = self._req(b"attachment; filename=out" + self.test_image.extension)
629629

630630
headers = channel.headers
631631

0 commit comments

Comments
 (0)