Skip to content

Commit 4724d7f

Browse files
authored
Fix TOCTOU symlink vulnerability in lock file creation (#461)
1 parent cb69414 commit 4724d7f

File tree

2 files changed

+39
-1
lines changed

2 files changed

+39
-1
lines changed

src/filelock/_unix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class UnixFileLock(BaseFileLock):
3838

3939
def _acquire(self) -> None:
4040
ensure_directory_exists(self.lock_file)
41-
open_flags = os.O_RDWR | os.O_TRUNC
41+
open_flags = os.O_RDWR | os.O_TRUNC | os.O_NOFOLLOW
4242
if not Path(self.lock_file).exists():
4343
open_flags |= os.O_CREAT
4444
fd = os.open(self.lock_file, open_flags, self._context.mode)

src/filelock/_windows.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,52 @@
1111
from ._util import ensure_directory_exists, raise_on_not_writable_file
1212

1313
if sys.platform == "win32": # pragma: win32 cover
14+
import ctypes
1415
import msvcrt
16+
from ctypes import wintypes
17+
18+
# Windows API constants for reparse point detection
19+
FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400
20+
INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF
21+
22+
# Load kernel32.dll
23+
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
24+
_kernel32.GetFileAttributesW.argtypes = [wintypes.LPCWSTR]
25+
_kernel32.GetFileAttributesW.restype = wintypes.DWORD
26+
27+
def _is_reparse_point(path: str) -> bool:
28+
"""
29+
Check if a path is a reparse point (symlink, junction, etc.) on Windows.
30+
31+
:param path: Path to check
32+
:return: True if path is a reparse point, False otherwise
33+
:raises OSError: If GetFileAttributesW fails for reasons other than file-not-found
34+
"""
35+
attrs = _kernel32.GetFileAttributesW(path)
36+
if attrs == INVALID_FILE_ATTRIBUTES:
37+
# File doesn't exist yet - that's fine, we'll create it
38+
err = ctypes.get_last_error()
39+
if err == 2: # noqa: PLR2004 # ERROR_FILE_NOT_FOUND
40+
return False
41+
if err == 3: # noqa: PLR2004 # ERROR_PATH_NOT_FOUND
42+
return False
43+
# Some other error - let caller handle it
44+
return False
45+
return bool(attrs & FILE_ATTRIBUTE_REPARSE_POINT)
1546

1647
class WindowsFileLock(BaseFileLock):
1748
"""Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems."""
1849

1950
def _acquire(self) -> None:
2051
raise_on_not_writable_file(self.lock_file)
2152
ensure_directory_exists(self.lock_file)
53+
54+
# Security check: Refuse to open reparse points (symlinks, junctions)
55+
# This prevents TOCTOU symlink attacks (CVE-TBD)
56+
if _is_reparse_point(self.lock_file):
57+
msg = f"Lock file is a reparse point (symlink/junction): {self.lock_file}"
58+
raise OSError(msg)
59+
2260
flags = (
2361
os.O_RDWR # open for read and write
2462
| os.O_CREAT # create file if not exists

0 commit comments

Comments
 (0)