@@ -43,47 +43,75 @@ def _wrapped(self: "MediaFilePaths", *args: Any, **kwargs: Any) -> str:
43
43
)
44
44
45
45
46
- def _wrap_with_jail_check (func : GetPathMethod ) -> GetPathMethod :
46
+ def _wrap_with_jail_check (relative : bool ) -> Callable [[ GetPathMethod ], GetPathMethod ] :
47
47
"""Wraps a path-returning method to check that the returned path(s) do not escape
48
48
the media store directory.
49
49
50
+ The path-returning method may return either a single path, or a list of paths.
51
+
50
52
The check is not expected to ever fail, unless `func` is missing a call to
51
53
`_validate_path_component`, or `_validate_path_component` is buggy.
52
54
53
55
Args:
54
- func: The `MediaFilePaths` method to wrap. The method may return either a single
55
- path, or a list of paths. Returned paths may be either absolute or relative .
56
+ relative: A boolean indicating whether the wrapped method returns paths relative
57
+ to the media store directory .
56
58
57
59
Returns:
58
- The method, wrapped with a check to ensure that the returned path(s) lie within
59
- the media store directory. Raises a `ValueError` if the check fails.
60
+ A method which will wrap a path-returning method, adding a check to ensure that
61
+ the returned path(s) lie within the media store directory. The check will raise
62
+ a `ValueError` if it fails.
60
63
"""
61
64
62
- @functools .wraps (func )
63
- def _wrapped (
64
- self : "MediaFilePaths" , * args : Any , ** kwargs : Any
65
- ) -> Union [str , List [str ]]:
66
- path_or_paths = func (self , * args , ** kwargs )
67
-
68
- if isinstance (path_or_paths , list ):
69
- paths_to_check = path_or_paths
70
- else :
71
- paths_to_check = [path_or_paths ]
72
-
73
- for path in paths_to_check :
74
- # path may be an absolute or relative path, depending on the method being
75
- # wrapped. When "appending" an absolute path, `os.path.join` discards the
76
- # previous path, which is desired here.
77
- normalized_path = os .path .normpath (os .path .join (self .real_base_path , path ))
78
- if (
79
- os .path .commonpath ([normalized_path , self .real_base_path ])
80
- != self .real_base_path
81
- ):
82
- raise ValueError (f"Invalid media store path: { path !r} " )
83
-
84
- return path_or_paths
85
-
86
- return cast (GetPathMethod , _wrapped )
65
+ def _wrap_with_jail_check_inner (func : GetPathMethod ) -> GetPathMethod :
66
+ @functools .wraps (func )
67
+ def _wrapped (
68
+ self : "MediaFilePaths" , * args : Any , ** kwargs : Any
69
+ ) -> Union [str , List [str ]]:
70
+ path_or_paths = func (self , * args , ** kwargs )
71
+
72
+ if isinstance (path_or_paths , list ):
73
+ paths_to_check = path_or_paths
74
+ else :
75
+ paths_to_check = [path_or_paths ]
76
+
77
+ for path in paths_to_check :
78
+ # Construct the path that will ultimately be used.
79
+ # We cannot guess whether `path` is relative to the media store
80
+ # directory, since the media store directory may itself be a relative
81
+ # path.
82
+ if relative :
83
+ path = os .path .join (self .base_path , path )
84
+ normalized_path = os .path .normpath (path )
85
+
86
+ # Now that `normpath` has eliminated `../`s and `./`s from the path,
87
+ # `os.path.commonpath` can be used to check whether it lies within the
88
+ # media store directory.
89
+ if (
90
+ os .path .commonpath ([normalized_path , self .normalized_base_path ])
91
+ != self .normalized_base_path
92
+ ):
93
+ # The path resolves to outside the media store directory,
94
+ # or `self.base_path` is `.`, which is an unlikely configuration.
95
+ raise ValueError (f"Invalid media store path: { path !r} " )
96
+
97
+ # Note that `os.path.normpath`/`abspath` has a subtle caveat:
98
+ # `a/b/c/../c` will normalize to `a/b/c`, but the former refers to a
99
+ # different path if `a/b/c` is a symlink. That is, the check above is
100
+ # not perfect and may allow a certain restricted subset of untrustworthy
101
+ # paths through. Since the check above is secondary to the main
102
+ # `_validate_path_component` checks, it's less important for it to be
103
+ # perfect.
104
+ #
105
+ # As an alternative, `os.path.realpath` will resolve symlinks, but
106
+ # proves problematic if there are symlinks inside the media store.
107
+ # eg. if `url_store/` is symlinked to elsewhere, its canonical path
108
+ # won't match that of the main media store directory.
109
+
110
+ return path_or_paths
111
+
112
+ return cast (GetPathMethod , _wrapped )
113
+
114
+ return _wrap_with_jail_check_inner
87
115
88
116
89
117
ALLOWED_CHARACTERS = set (
@@ -127,9 +155,7 @@ class MediaFilePaths:
127
155
128
156
def __init__ (self , primary_base_path : str ):
129
157
self .base_path = primary_base_path
130
-
131
- # The media store directory, with all symlinks resolved.
132
- self .real_base_path = os .path .realpath (primary_base_path )
158
+ self .normalized_base_path = os .path .normpath (self .base_path )
133
159
134
160
# Refuse to initialize if paths cannot be validated correctly for the current
135
161
# platform.
@@ -140,7 +166,7 @@ def __init__(self, primary_base_path: str):
140
166
# for certain homeservers there, since ":"s aren't allowed in paths.
141
167
assert os .name == "posix"
142
168
143
- @_wrap_with_jail_check
169
+ @_wrap_with_jail_check ( relative = True )
144
170
def local_media_filepath_rel (self , media_id : str ) -> str :
145
171
return os .path .join (
146
172
"local_content" ,
@@ -151,7 +177,7 @@ def local_media_filepath_rel(self, media_id: str) -> str:
151
177
152
178
local_media_filepath = _wrap_in_base_path (local_media_filepath_rel )
153
179
154
- @_wrap_with_jail_check
180
+ @_wrap_with_jail_check ( relative = True )
155
181
def local_media_thumbnail_rel (
156
182
self , media_id : str , width : int , height : int , content_type : str , method : str
157
183
) -> str :
@@ -167,7 +193,7 @@ def local_media_thumbnail_rel(
167
193
168
194
local_media_thumbnail = _wrap_in_base_path (local_media_thumbnail_rel )
169
195
170
- @_wrap_with_jail_check
196
+ @_wrap_with_jail_check ( relative = False )
171
197
def local_media_thumbnail_dir (self , media_id : str ) -> str :
172
198
"""
173
199
Retrieve the local store path of thumbnails of a given media_id
@@ -185,7 +211,7 @@ def local_media_thumbnail_dir(self, media_id: str) -> str:
185
211
_validate_path_component (media_id [4 :]),
186
212
)
187
213
188
- @_wrap_with_jail_check
214
+ @_wrap_with_jail_check ( relative = True )
189
215
def remote_media_filepath_rel (self , server_name : str , file_id : str ) -> str :
190
216
return os .path .join (
191
217
"remote_content" ,
@@ -197,7 +223,7 @@ def remote_media_filepath_rel(self, server_name: str, file_id: str) -> str:
197
223
198
224
remote_media_filepath = _wrap_in_base_path (remote_media_filepath_rel )
199
225
200
- @_wrap_with_jail_check
226
+ @_wrap_with_jail_check ( relative = True )
201
227
def remote_media_thumbnail_rel (
202
228
self ,
203
229
server_name : str ,
@@ -223,7 +249,7 @@ def remote_media_thumbnail_rel(
223
249
# Legacy path that was used to store thumbnails previously.
224
250
# Should be removed after some time, when most of the thumbnails are stored
225
251
# using the new path.
226
- @_wrap_with_jail_check
252
+ @_wrap_with_jail_check ( relative = True )
227
253
def remote_media_thumbnail_rel_legacy (
228
254
self , server_name : str , file_id : str , width : int , height : int , content_type : str
229
255
) -> str :
@@ -238,6 +264,7 @@ def remote_media_thumbnail_rel_legacy(
238
264
_validate_path_component (file_name ),
239
265
)
240
266
267
+ @_wrap_with_jail_check (relative = False )
241
268
def remote_media_thumbnail_dir (self , server_name : str , file_id : str ) -> str :
242
269
return os .path .join (
243
270
self .base_path ,
@@ -248,7 +275,7 @@ def remote_media_thumbnail_dir(self, server_name: str, file_id: str) -> str:
248
275
_validate_path_component (file_id [4 :]),
249
276
)
250
277
251
- @_wrap_with_jail_check
278
+ @_wrap_with_jail_check ( relative = True )
252
279
def url_cache_filepath_rel (self , media_id : str ) -> str :
253
280
if NEW_FORMAT_ID_RE .match (media_id ):
254
281
# Media id is of the form <DATE><RANDOM_STRING>
@@ -268,7 +295,7 @@ def url_cache_filepath_rel(self, media_id: str) -> str:
268
295
269
296
url_cache_filepath = _wrap_in_base_path (url_cache_filepath_rel )
270
297
271
- @_wrap_with_jail_check
298
+ @_wrap_with_jail_check ( relative = False )
272
299
def url_cache_filepath_dirs_to_delete (self , media_id : str ) -> List [str ]:
273
300
"The dirs to try and remove if we delete the media_id file"
274
301
if NEW_FORMAT_ID_RE .match (media_id ):
@@ -290,7 +317,7 @@ def url_cache_filepath_dirs_to_delete(self, media_id: str) -> List[str]:
290
317
),
291
318
]
292
319
293
- @_wrap_with_jail_check
320
+ @_wrap_with_jail_check ( relative = True )
294
321
def url_cache_thumbnail_rel (
295
322
self , media_id : str , width : int , height : int , content_type : str , method : str
296
323
) -> str :
@@ -318,7 +345,7 @@ def url_cache_thumbnail_rel(
318
345
319
346
url_cache_thumbnail = _wrap_in_base_path (url_cache_thumbnail_rel )
320
347
321
- @_wrap_with_jail_check
348
+ @_wrap_with_jail_check ( relative = True )
322
349
def url_cache_thumbnail_directory_rel (self , media_id : str ) -> str :
323
350
# Media id is of the form <DATE><RANDOM_STRING>
324
351
# E.g.: 2017-09-28-fsdRDt24DS234dsf
@@ -341,7 +368,7 @@ def url_cache_thumbnail_directory_rel(self, media_id: str) -> str:
341
368
url_cache_thumbnail_directory_rel
342
369
)
343
370
344
- @_wrap_with_jail_check
371
+ @_wrap_with_jail_check ( relative = False )
345
372
def url_cache_thumbnail_dirs_to_delete (self , media_id : str ) -> List [str ]:
346
373
"The dirs to try and remove if we delete the media_id thumbnails"
347
374
# Media id is of the form <DATE><RANDOM_STRING>
0 commit comments