Skip to content

Commit f54fe98

Browse files
committed
safe_join prevents Windows special device names in multi-segment paths
1 parent d005985 commit f54fe98

File tree

3 files changed

+32
-16
lines changed

3 files changed

+32
-16
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Version 3.1.6
55

66
Unreleased
77

8+
- ``safe_join`` on Windows does not allow special devices names in
9+
multi-segment paths. :ghsa:`29vq-49wr-vm6x`
10+
811

912
Version 3.1.5
1013
-------------

src/werkzeug/security.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
DEFAULT_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)

tests/test_security.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_safe_join_empty_trusted():
7373

7474

7575
@pytest.mark.parametrize(
76-
"name", ["CON", "CON.txt", "CON.txt.html", "CON ", "CON . txt"]
76+
"name", ["CON", "CON.txt", "CON.txt.html", "CON ", "CON . txt", "b/CON"]
7777
)
7878
def test_safe_join_windows_special(monkeypatch: pytest.MonkeyPatch, name: str) -> None:
7979
"""Windows special device name is not allowed on Windows."""

0 commit comments

Comments
 (0)