VYPR
Moderate severityOSV Advisory· Published Jan 8, 2026· Updated Jan 8, 2026

Werkzeug safe_join() allows Windows special device names with compound extensions

CVE-2026-21860

Description

Werkzeug is a comprehensive WSGI web application library. Prior to version 3.1.5, Werkzeug's safe_join function allows path segments with Windows device names that have file extensions or trailing spaces. On Windows, there are special device names such as CON, AUX, etc that are implicitly present and readable in every directory. Windows still accepts them with any file extension, such as CON.txt, or trailing spaces such as CON. This issue has been patched in version 3.1.5.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
WerkzeugPyPI
< 3.1.53.1.5

Affected products

1

Patches

1
7ae1d254e04a

Merge commit from fork

https://github.com/pallets/werkzeugDavid LordJan 8, 2026via ghsa
3 files changed · +23 8
  • CHANGES.rst+2 0 modified
    @@ -5,6 +5,8 @@ Version 3.1.5
     
     Unreleased
     
    +-   ``safe_join`` on Windows does not allow more special device names, regardless
    +    of extension or surrounding spaces. :ghsa:`87hc-h4r5-73f7`
     -   The multipart form parser handles a ``\r\n`` sequence at a chunk boundary.
         This fixes the previous attempt, which caused incorrect content lengths.
         :issue:`3065` :issue:`3077`
    
  • src/werkzeug/security.py+13 6 modified
    @@ -12,13 +12,16 @@
     _os_alt_seps: list[str] = list(
         sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/"
     )
    +# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html
     _windows_device_files = {
    -    "CON",
    -    "PRN",
         "AUX",
    +    "CON",
    +    "CONIN$",
    +    "CONOUT$",
    +    *(f"COM{c}" for c in "123456789¹²³"),
    +    *(f"LPT{c}" for c in "123456789¹²³"),
         "NUL",
    -    *(f"COM{i}" for i in range(10)),
    -    *(f"LPT{i}" for i in range(10)),
    +    "PRN",
     }
     
     
    @@ -148,8 +151,12 @@ def safe_join(directory: str, *pathnames: str) -> str | None:
             base directory.
         :return: A safe path, otherwise ``None``.
     
    +    .. versionchanged:: 3.1.5
    +        More special device names, regardless of extension or trailing spaces,
    +        are not allowed on Windows.
    +
         .. versionchanged:: 3.1.4
    -        Special device names are disallowed on Windows.
    +        Special device names are not allowed on Windows.
         """
         if not directory:
             # Ensure we end up with ./path if directory="" is given,
    @@ -166,7 +173,7 @@ def safe_join(directory: str, *pathnames: str) -> str | None:
                 any(sep in filename for sep in _os_alt_seps)
                 or (
                     os.name == "nt"
    -                and os.path.splitext(filename)[0].upper() in _windows_device_files
    +                and filename.partition(".")[0].strip().upper() in _windows_device_files
                 )
                 or os.path.isabs(filename)
                 # ntpath.isabs doesn't catch this on Python < 3.11
    
  • tests/test_security.py+8 2 modified
    @@ -72,9 +72,15 @@ def test_safe_join_empty_trusted():
         assert safe_join("", "c:test.txt") == "./c:test.txt"
     
     
    -def test_safe_join_windows_special(monkeypatch: pytest.MonkeyPatch) -> None:
    +@pytest.mark.parametrize(
    +    "name", ["CON", "CON.txt", "CON.txt.html", "CON  ", "CON . txt"]
    +)
    +def test_safe_join_windows_special(monkeypatch: pytest.MonkeyPatch, name: str) -> None:
         """Windows special device name is not allowed on Windows."""
         monkeypatch.setattr("os.name", "nt")
    -    assert safe_join("a", "CON") is None
    +    assert safe_join("a", name) is None
    +
    +
    +def test_safe_join_not_windows_special(monkeypatch: pytest.MonkeyPatch) -> None:
         monkeypatch.setattr("os.name", "posix")
         assert safe_join("a", "CON") == "a/CON"
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

4

News mentions

0

No linked articles in our index yet.