diff --git a/CHANGES.rst b/CHANGES.rst
index f55d192c2..f709fb5cd 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,6 +8,9 @@ Unreleased
 -   Fix how ``max_form_memory_size`` is applied when parsing large non-file
     fields. :ghsa:`q34m-jh98-gwm2`
 
+-   ``safe_join`` catches certain paths on Windows that were not caught by
+    ``ntpath.isabs`` on Python < 3.11. :ghsa:`f9vj-2wh5-fj8j`
+
 
 Version 3.0.5
 -------------
diff --git a/src/werkzeug/security.py b/src/werkzeug/security.py
index 9999509d1..997597990 100644
--- a/src/werkzeug/security.py
+++ b/src/werkzeug/security.py
@@ -151,6 +151,8 @@ def safe_join(directory: str, *pathnames: str) -> str | None:
         if (
             any(sep in filename for sep in _os_alt_seps)
             or os.path.isabs(filename)
+            # ntpath.isabs doesn't catch this on Python < 3.11
+            or filename.startswith("/")
             or filename == ".."
             or filename.startswith("../")
         ):
diff --git a/tests/test_security.py b/tests/test_security.py
index 6fad089a7..3ce741a99 100644
--- a/tests/test_security.py
+++ b/tests/test_security.py
@@ -1,5 +1,4 @@
 import os
-import posixpath
 import sys
 
 import pytest
@@ -47,11 +46,17 @@ def test_invalid_method():
         generate_password_hash("secret", "sha256")
 
 
-def test_safe_join():
-    assert safe_join("foo", "bar/baz") == posixpath.join("foo", "bar/baz")
-    assert safe_join("foo", "../bar/baz") is None
-    if os.name == "nt":
-        assert safe_join("foo", "foo\\bar") is None
+@pytest.mark.parametrize(
+    ("path", "expect"),
+    [
+        ("b/c", "a/b/c"),
+        ("../b/c", None),
+        ("b\\c", None if os.name == "nt" else "a/b\\c"),
+        ("//b/c", None),
+    ],
+)
+def test_safe_join(path, expect):
+    assert safe_join("a", path) == expect
 
 
 def test_safe_join_os_sep():