diff --git a/docs/index.rst b/docs/index.rst index fb55b871..87e1f397 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -216,6 +216,22 @@ The :class:`SoftFileLock ` only watches the existence of portable, but also more prone to dead locks if the application crashes. You can simply delete the lock file in such cases. +.. warning:: + + **Security Consideration - TOCTOU Vulnerability**: On platforms without ``O_NOFOLLOW`` support + (such as GraalPy), :class:`SoftFileLock ` may be vulnerable to symlink-based + Time-of-Check-Time-of-Use (TOCTOU) attacks. An attacker with local filesystem access could create + a symlink at the lock file path during the small race window between permission validation and file + creation. + + On most modern platforms with ``O_NOFOLLOW`` support, this vulnerability is mitigated by refusing + to follow symlinks when creating the lock file. + + For security-sensitive applications, prefer :class:`UnixFileLock ` or + :class:`WindowsFileLock ` which provide stronger guarantees via OS-level + file locking. :class:`SoftFileLock ` should only be used as a fallback mechanism + on platforms where OS-level locking primitives are unavailable. + Asyncio support --------------- diff --git a/src/filelock/_soft.py b/src/filelock/_soft.py index 28c67f74..93709c5c 100644 --- a/src/filelock/_soft.py +++ b/src/filelock/_soft.py @@ -16,13 +16,15 @@ class SoftFileLock(BaseFileLock): def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) ensure_directory_exists(self.lock_file) - # first check for exists and read-only mode as the open will mask this case as EEXIST flags = ( os.O_WRONLY # open for writing only | os.O_CREAT | os.O_EXCL # together with above raise EEXIST if the file specified by filename exists | os.O_TRUNC # truncate the file to zero byte ) + o_nofollow = getattr(os, "O_NOFOLLOW", None) + if o_nofollow is not None: + flags |= o_nofollow try: file_handler = os.open(self.lock_file, flags, self._context.mode) except OSError as exception: # re-raise unless expected exception