1010DEFAULT_PBKDF2_ITERATIONS = 1_000_000
1111
1212_os_alt_seps : list [str ] = list (
13- sep for sep in [os .sep , os .path . altsep ] if sep is not None and sep != "/"
13+ sep for sep in [os .sep , os .altsep ] if sep is not None and sep != "/"
1414)
1515# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html
1616_windows_device_files = {
@@ -142,15 +142,23 @@ def check_password_hash(pwhash: str, password: str) -> bool:
142142 return hmac .compare_digest (_hash_internal (method , salt , password )[0 ], hashval )
143143
144144
145- def safe_join (directory : str , * pathnames : str ) -> str | None :
146- """Safely join zero or more untrusted path components to a base
145+ def safe_join (directory : str , * untrusted : str ) -> str | None :
146+ """Safely join zero or more untrusted path components to a trusted base
147147 directory to avoid escaping the base directory.
148148
149+ The untrusted path is assumed to be from/for a URL, such as for serving
150+ files. Therefore, it should only use the forward slash ``/`` path separator,
151+ and will be joined using that separator. On Windows, the backslash ``\\ ``
152+ separator is not allowed.
153+
149154 :param directory: The trusted base directory.
150- :param pathnames : The untrusted path components relative to the
155+ :param untrusted : The untrusted path components relative to the
151156 base directory.
152157 :return: A safe path, otherwise ``None``.
153158
159+ .. versionchanged:: 3.1.6
160+ Special device names in multi-segment paths are not allowed on Windows.
161+
154162 .. versionchanged:: 3.1.5
155163 More special device names, regardless of extension or trailing spaces,
156164 are not allowed on Windows.
@@ -165,24 +173,29 @@ def safe_join(directory: str, *pathnames: str) -> str | None:
165173
166174 parts = [directory ]
167175
168- for filename in pathnames :
169- if filename != "" :
170- filename = posixpath .normpath (filename )
176+ for part in untrusted :
177+ if not part :
178+ continue
179+
180+ part = posixpath .normpath (part )
171181
172182 if (
173- any (sep in filename for sep in _os_alt_seps )
183+ os .path .isabs (part )
184+ # ntpath.isabs doesn't catch this
185+ or part .startswith ("/" )
186+ or part == ".."
187+ or part .startswith ("../" )
188+ or any (sep in part for sep in _os_alt_seps )
174189 or (
175190 os .name == "nt"
176- and filename .partition ("." )[0 ].strip ().upper () in _windows_device_files
191+ and any (
192+ p .partition ("." )[0 ].strip ().upper () in _windows_device_files
193+ for p in part .split ("/" )
194+ )
177195 )
178- or os .path .isabs (filename )
179- # ntpath.isabs doesn't catch this on Python < 3.11
180- or filename .startswith ("/" )
181- or filename == ".."
182- or filename .startswith ("../" )
183196 ):
184197 return None
185198
186- parts .append (filename )
199+ parts .append (part )
187200
188201 return posixpath .join (* parts )
0 commit comments