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

Commit ab4535b

Browse files
authored
Add config option to prevent media downloads from listed domains. (#15197)
This stops media (and thumbnails) from being accessed from the listed domains. It does not delete any already locally cached media, but will prevent accessing it. Note that admin APIs are unaffected by this change.
1 parent 266d287 commit ab4535b

File tree

6 files changed

+186
-0
lines changed

6 files changed

+186
-0
lines changed

changelog.d/15197.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add an option to prevent media downloads from configured domains.

docs/usage/configuration/config_documentation.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,6 +1768,30 @@ Example configuration:
17681768
max_image_pixels: 35M
17691769
```
17701770
---
1771+
### `prevent_media_downloads_from`
1772+
1773+
A list of domains to never download media from. Media from these
1774+
domains that is already downloaded will not be deleted, but will be
1775+
inaccessible to users. This option does not affect admin APIs trying
1776+
to download/operate on media.
1777+
1778+
This will not prevent the listed domains from accessing media themselves.
1779+
It simply prevents users on this server from downloading media originating
1780+
from the listed servers.
1781+
1782+
This will have no effect on media originating from the local server.
1783+
This only affects media downloaded from other Matrix servers, to
1784+
block domains from URL previews see [`url_preview_url_blacklist`](#url_preview_url_blacklist).
1785+
1786+
Defaults to an empty list (nothing blocked).
1787+
1788+
Example configuration:
1789+
```yaml
1790+
prevent_media_downloads_from:
1791+
- evil.example.org
1792+
- evil2.example.org
1793+
```
1794+
---
17711795
### `dynamic_thumbnails`
17721796

17731797
Whether to generate new thumbnails on the fly to precisely match

synapse/config/repository.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
137137
self.max_image_pixels = self.parse_size(config.get("max_image_pixels", "32M"))
138138
self.max_spider_size = self.parse_size(config.get("max_spider_size", "10M"))
139139

140+
self.prevent_media_downloads_from = config.get(
141+
"prevent_media_downloads_from", []
142+
)
143+
140144
self.media_store_path = self.ensure_directory(
141145
config.get("media_store_path", "media_store")
142146
)

synapse/media/media_repository.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ def __init__(self, hs: "HomeServer"):
9393
self.federation_domain_whitelist = (
9494
hs.config.federation.federation_domain_whitelist
9595
)
96+
self.prevent_media_downloads_from = hs.config.media.prevent_media_downloads_from
9697

9798
# List of StorageProviders where we should search for media and
9899
# potentially upload to.
@@ -276,6 +277,14 @@ async def get_remote_media(
276277
):
277278
raise FederationDeniedError(server_name)
278279

280+
# Don't let users download media from domains listed in the config, even
281+
# if we might have the media to serve. This is Trust & Safety tooling to
282+
# block some servers' media from being accessible to local users.
283+
# See `prevent_media_downloads_from` config docs for more info.
284+
if server_name in self.prevent_media_downloads_from:
285+
respond_404(request)
286+
return
287+
279288
self.mark_recently_accessed(server_name, media_id)
280289

281290
# We linearize here to ensure that we don't try and download remote

synapse/rest/media/thumbnail_resource.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def __init__(
6060
self.media_storage = media_storage
6161
self.dynamic_thumbnails = hs.config.media.dynamic_thumbnails
6262
self._is_mine_server_name = hs.is_mine_server_name
63+
self.prevent_media_downloads_from = hs.config.media.prevent_media_downloads_from
6364

6465
async def _async_render_GET(self, request: SynapseRequest) -> None:
6566
set_cors_headers(request)
@@ -82,6 +83,14 @@ async def _async_render_GET(self, request: SynapseRequest) -> None:
8283
)
8384
self.media_repo.mark_recently_accessed(None, media_id)
8485
else:
86+
# Don't let users download media from configured domains, even if it
87+
# is already downloaded. This is Trust & Safety tooling to make some
88+
# media inaccessible to local users.
89+
# See `prevent_media_downloads_from` config docs for more info.
90+
if server_name in self.prevent_media_downloads_from:
91+
respond_404(request)
92+
return
93+
8594
if self.dynamic_thumbnails:
8695
await self._select_or_generate_remote_thumbnail(
8796
request, server_name, media_id, width, height, method, m_type
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Copyright 2023 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
from typing import Dict
15+
16+
from twisted.test.proto_helpers import MemoryReactor
17+
from twisted.web.resource import Resource
18+
19+
from synapse.media._base import FileInfo
20+
from synapse.server import HomeServer
21+
from synapse.util import Clock
22+
23+
from tests import unittest
24+
from tests.test_utils import SMALL_PNG
25+
from tests.unittest import override_config
26+
27+
28+
class MediaDomainBlockingTests(unittest.HomeserverTestCase):
29+
remote_media_id = "doesnotmatter"
30+
remote_server_name = "evil.com"
31+
32+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
33+
self.store = hs.get_datastores().main
34+
35+
# Inject a piece of media. We'll use this to ensure we're returning a sane
36+
# response when we're not supposed to block it, distinguishing a media block
37+
# from a regular 404.
38+
file_id = "abcdefg12345"
39+
file_info = FileInfo(server_name=self.remote_server_name, file_id=file_id)
40+
with hs.get_media_repository().media_storage.store_into_file(file_info) as (
41+
f,
42+
fname,
43+
finish,
44+
):
45+
f.write(SMALL_PNG)
46+
self.get_success(finish())
47+
48+
self.get_success(
49+
self.store.store_cached_remote_media(
50+
origin=self.remote_server_name,
51+
media_id=self.remote_media_id,
52+
media_type="image/png",
53+
media_length=1,
54+
time_now_ms=clock.time_msec(),
55+
upload_name="test.png",
56+
filesystem_id=file_id,
57+
)
58+
)
59+
60+
def create_resource_dict(self) -> Dict[str, Resource]:
61+
# We need to manually set the resource tree to include media, the
62+
# default only does `/_matrix/client` APIs.
63+
return {"/_matrix/media": self.hs.get_media_repository_resource()}
64+
65+
@override_config(
66+
{
67+
# Disable downloads from the domain we'll be trying to download from.
68+
# Should result in a 404.
69+
"prevent_media_downloads_from": ["evil.com"]
70+
}
71+
)
72+
def test_cannot_download_blocked_media(self) -> None:
73+
"""
74+
Tests to ensure that remote media which is blocked cannot be downloaded.
75+
"""
76+
response = self.make_request(
77+
"GET",
78+
f"/_matrix/media/v3/download/evil.com/{self.remote_media_id}",
79+
shorthand=False,
80+
)
81+
self.assertEqual(response.code, 404)
82+
83+
@override_config(
84+
{
85+
# Disable downloads from a domain we won't be requesting downloads from.
86+
# This proves we haven't broken anything.
87+
"prevent_media_downloads_from": ["not-listed.com"]
88+
}
89+
)
90+
def test_remote_media_normally_unblocked(self) -> None:
91+
"""
92+
Tests to ensure that remote media is normally able to be downloaded
93+
when no domain block is in place.
94+
"""
95+
response = self.make_request(
96+
"GET",
97+
f"/_matrix/media/v3/download/evil.com/{self.remote_media_id}",
98+
shorthand=False,
99+
)
100+
self.assertEqual(response.code, 200)
101+
102+
@override_config(
103+
{
104+
# Disable downloads from the domain we'll be trying to download from.
105+
# Should result in a 404.
106+
"prevent_media_downloads_from": ["evil.com"],
107+
"dynamic_thumbnails": True,
108+
}
109+
)
110+
def test_cannot_download_blocked_media_thumbnail(self) -> None:
111+
"""
112+
Same test as test_cannot_download_blocked_media but for thumbnails.
113+
"""
114+
response = self.make_request(
115+
"GET",
116+
f"/_matrix/media/v3/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
117+
shorthand=False,
118+
content={"width": 100, "height": 100},
119+
)
120+
self.assertEqual(response.code, 404)
121+
122+
@override_config(
123+
{
124+
# Disable downloads from a domain we won't be requesting downloads from.
125+
# This proves we haven't broken anything.
126+
"prevent_media_downloads_from": ["not-listed.com"],
127+
"dynamic_thumbnails": True,
128+
}
129+
)
130+
def test_remote_media_thumbnail_normally_unblocked(self) -> None:
131+
"""
132+
Same test as test_remote_media_normally_unblocked but for thumbnails.
133+
"""
134+
response = self.make_request(
135+
"GET",
136+
f"/_matrix/media/v3/thumbnail/evil.com/{self.remote_media_id}?width=100&height=100",
137+
shorthand=False,
138+
)
139+
self.assertEqual(response.code, 200)

0 commit comments

Comments
 (0)