Skip to content

Commit 41b42dd

Browse files
gaborbernatclaude
andauthored
Fix TOCTOU symlink vulnerability in SoftFileLock (#465)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent f2e7d40 commit 41b42dd

2 files changed

Lines changed: 19 additions & 1 deletion

File tree

docs/index.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,22 @@ The :class:`SoftFileLock <filelock.SoftFileLock>` only watches the existence of
216216
portable, but also more prone to dead locks if the application crashes. You can simply delete the lock file in such
217217
cases.
218218

219+
.. warning::
220+
221+
**Security Consideration - TOCTOU Vulnerability**: On platforms without ``O_NOFOLLOW`` support
222+
(such as GraalPy), :class:`SoftFileLock <filelock.SoftFileLock>` may be vulnerable to symlink-based
223+
Time-of-Check-Time-of-Use (TOCTOU) attacks. An attacker with local filesystem access could create
224+
a symlink at the lock file path during the small race window between permission validation and file
225+
creation.
226+
227+
On most modern platforms with ``O_NOFOLLOW`` support, this vulnerability is mitigated by refusing
228+
to follow symlinks when creating the lock file.
229+
230+
For security-sensitive applications, prefer :class:`UnixFileLock <filelock.UnixFileLock>` or
231+
:class:`WindowsFileLock <filelock.WindowsFileLock>` which provide stronger guarantees via OS-level
232+
file locking. :class:`SoftFileLock <filelock.SoftFileLock>` should only be used as a fallback mechanism
233+
on platforms where OS-level locking primitives are unavailable.
234+
219235
Asyncio support
220236
---------------
221237

src/filelock/_soft.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ class SoftFileLock(BaseFileLock):
1616
def _acquire(self) -> None:
1717
raise_on_not_writable_file(self.lock_file)
1818
ensure_directory_exists(self.lock_file)
19-
# first check for exists and read-only mode as the open will mask this case as EEXIST
2019
flags = (
2120
os.O_WRONLY # open for writing only
2221
| os.O_CREAT
2322
| os.O_EXCL # together with above raise EEXIST if the file specified by filename exists
2423
| os.O_TRUNC # truncate the file to zero byte
2524
)
25+
o_nofollow = getattr(os, "O_NOFOLLOW", None)
26+
if o_nofollow is not None:
27+
flags |= o_nofollow
2628
try:
2729
file_handler = os.open(self.lock_file, flags, self._context.mode)
2830
except OSError as exception: # re-raise unless expected exception

0 commit comments

Comments
 (0)