@@ -43,47 +43,75 @@ def _wrapped(self: "MediaFilePaths", *args: Any, **kwargs: Any) -> str:
4343)
4444
4545
46- def _wrap_with_jail_check (func : GetPathMethod ) -> GetPathMethod :
46+ def _wrap_with_jail_check (relative : bool ) -> Callable [[ GetPathMethod ], GetPathMethod ] :
4747 """Wraps a path-returning method to check that the returned path(s) do not escape
4848 the media store directory.
4949
50+ The path-returning method may return either a single path, or a list of paths.
51+
5052 The check is not expected to ever fail, unless `func` is missing a call to
5153 `_validate_path_component`, or `_validate_path_component` is buggy.
5254
5355 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 .
5658
5759 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.
6063 """
6164
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
87115
88116
89117ALLOWED_CHARACTERS = set (
@@ -127,9 +155,7 @@ class MediaFilePaths:
127155
128156 def __init__ (self , primary_base_path : str ):
129157 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 )
133159
134160 # Refuse to initialize if paths cannot be validated correctly for the current
135161 # platform.
@@ -140,7 +166,7 @@ def __init__(self, primary_base_path: str):
140166 # for certain homeservers there, since ":"s aren't allowed in paths.
141167 assert os .name == "posix"
142168
143- @_wrap_with_jail_check
169+ @_wrap_with_jail_check ( relative = True )
144170 def local_media_filepath_rel (self , media_id : str ) -> str :
145171 return os .path .join (
146172 "local_content" ,
@@ -151,7 +177,7 @@ def local_media_filepath_rel(self, media_id: str) -> str:
151177
152178 local_media_filepath = _wrap_in_base_path (local_media_filepath_rel )
153179
154- @_wrap_with_jail_check
180+ @_wrap_with_jail_check ( relative = True )
155181 def local_media_thumbnail_rel (
156182 self , media_id : str , width : int , height : int , content_type : str , method : str
157183 ) -> str :
@@ -167,7 +193,7 @@ def local_media_thumbnail_rel(
167193
168194 local_media_thumbnail = _wrap_in_base_path (local_media_thumbnail_rel )
169195
170- @_wrap_with_jail_check
196+ @_wrap_with_jail_check ( relative = False )
171197 def local_media_thumbnail_dir (self , media_id : str ) -> str :
172198 """
173199 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:
185211 _validate_path_component (media_id [4 :]),
186212 )
187213
188- @_wrap_with_jail_check
214+ @_wrap_with_jail_check ( relative = True )
189215 def remote_media_filepath_rel (self , server_name : str , file_id : str ) -> str :
190216 return os .path .join (
191217 "remote_content" ,
@@ -197,7 +223,7 @@ def remote_media_filepath_rel(self, server_name: str, file_id: str) -> str:
197223
198224 remote_media_filepath = _wrap_in_base_path (remote_media_filepath_rel )
199225
200- @_wrap_with_jail_check
226+ @_wrap_with_jail_check ( relative = True )
201227 def remote_media_thumbnail_rel (
202228 self ,
203229 server_name : str ,
@@ -223,7 +249,7 @@ def remote_media_thumbnail_rel(
223249 # Legacy path that was used to store thumbnails previously.
224250 # Should be removed after some time, when most of the thumbnails are stored
225251 # using the new path.
226- @_wrap_with_jail_check
252+ @_wrap_with_jail_check ( relative = True )
227253 def remote_media_thumbnail_rel_legacy (
228254 self , server_name : str , file_id : str , width : int , height : int , content_type : str
229255 ) -> str :
@@ -238,6 +264,7 @@ def remote_media_thumbnail_rel_legacy(
238264 _validate_path_component (file_name ),
239265 )
240266
267+ @_wrap_with_jail_check (relative = False )
241268 def remote_media_thumbnail_dir (self , server_name : str , file_id : str ) -> str :
242269 return os .path .join (
243270 self .base_path ,
@@ -248,7 +275,7 @@ def remote_media_thumbnail_dir(self, server_name: str, file_id: str) -> str:
248275 _validate_path_component (file_id [4 :]),
249276 )
250277
251- @_wrap_with_jail_check
278+ @_wrap_with_jail_check ( relative = True )
252279 def url_cache_filepath_rel (self , media_id : str ) -> str :
253280 if NEW_FORMAT_ID_RE .match (media_id ):
254281 # Media id is of the form <DATE><RANDOM_STRING>
@@ -268,7 +295,7 @@ def url_cache_filepath_rel(self, media_id: str) -> str:
268295
269296 url_cache_filepath = _wrap_in_base_path (url_cache_filepath_rel )
270297
271- @_wrap_with_jail_check
298+ @_wrap_with_jail_check ( relative = False )
272299 def url_cache_filepath_dirs_to_delete (self , media_id : str ) -> List [str ]:
273300 "The dirs to try and remove if we delete the media_id file"
274301 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]:
290317 ),
291318 ]
292319
293- @_wrap_with_jail_check
320+ @_wrap_with_jail_check ( relative = True )
294321 def url_cache_thumbnail_rel (
295322 self , media_id : str , width : int , height : int , content_type : str , method : str
296323 ) -> str :
@@ -318,7 +345,7 @@ def url_cache_thumbnail_rel(
318345
319346 url_cache_thumbnail = _wrap_in_base_path (url_cache_thumbnail_rel )
320347
321- @_wrap_with_jail_check
348+ @_wrap_with_jail_check ( relative = True )
322349 def url_cache_thumbnail_directory_rel (self , media_id : str ) -> str :
323350 # Media id is of the form <DATE><RANDOM_STRING>
324351 # E.g.: 2017-09-28-fsdRDt24DS234dsf
@@ -341,7 +368,7 @@ def url_cache_thumbnail_directory_rel(self, media_id: str) -> str:
341368 url_cache_thumbnail_directory_rel
342369 )
343370
344- @_wrap_with_jail_check
371+ @_wrap_with_jail_check ( relative = False )
345372 def url_cache_thumbnail_dirs_to_delete (self , media_id : str ) -> List [str ]:
346373 "The dirs to try and remove if we delete the media_id thumbnails"
347374 # Media id is of the form <DATE><RANDOM_STRING>
0 commit comments