Threats affecting pkg:bitnami/dotnet
191,097 threats
Most recently published first
| Severity | CVSS | Signals | Threat | Source | Published | Score and star |
|---|---|---|---|---|---|---|
| Low | 2.5 | EPSS 0.2% | CVE-2026-112912pre-commit · filelock: Symlink swap during SoftFileLock's writability check leaks whether a path exists and is writable by the lock Low2.5EPSS 0.2%GCVE 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 | EPSS 0.1% | CVE-2026-112911pre-commit · filelock: StrictSoftFileLock and SoftReadWriteLock adopt a protocol directory another user created first, losing Medium6.3EPSS 0.1%GCVE 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 | EPSS 0.2% | CVE-2026-112909pre-commit · filelock: UnixFileLock truncates and chmods any regular file another user renames or hard-links onto the lock path Medium6.1EPSS 0.2%GCVE 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-112908pre-commit · 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 | 4.7 | EPSS 0.2% | CVE-2026-112906pre-commit · filelock: SoftFileLock evicts a fresh lock file whose watched age another local user's pre-dated file passed on Medium4.7EPSS 0.2%GCVE DatabaseCVE-2026-112906 `SoftFileLock` and `MarkerSoftFileLock` evict an unparsable marker (the lock file at `<path>`, whose presence says a process holds the lock) once a waiting contender has watched it unchanged for two seconds. `SoftFileLease` expires a lease the same way. The watch tells two looks apart by the marker's modification time (mtime) and inode number (the kernel's identity for a file, independent of its name) alone. 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), parks an empty file of its own at the lock path. It dates that file to a future tick of the kernel's coarse clock (a clock that advances in steps of 4 ms), lets the waiters of the user running the lock (the lock user below) watch it past two seconds, and unlinks it in that tick. On a filesystem that hands a freed inode number straight back (ext4, overlayfs on ext4) the lock user's next marker repeats both values. It inherits the time spent watching the other user's file, and a second waiter removes it while its owner holds the lock. Two of the lock user's processes then run the critical section together with no error. A `SoftFileLease` waiter takes a live lease that its owner wrote moments before, within milliseconds instead of after `lease_duration`. Affected versions: filelock >= 4.1.0 and <= 4.2.0 (finding `ANT-2026-RKC48J5B`). ## 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 the other user cannot steer. It needs ext4-style inode reuse, a coarse-clock timestamp tick the other user can predict, and two polls of the lock user inside a 4 ms tick and a ~75 µs create-to-write gap. The scanner measured 2 to 10 % of attempts; none of our own 250 attempts fired. `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, by mechanism. It is the same cross-UID primitive (a UID is a user account's numeric id) as the stale-break inode-reuse race reported in a separate advisory: an inode number reused after the other user's unlink in a sticky directory, which our review reproduced across UIDs, plus a forged future mtime. The scanner rated it high (CWE-694) and demonstrated it across UIDs in its own runs. It did not fire in 250 attempts of ours over ext4 with inode reuse confirmed, nor in the scanner-side triage, so its cross-user claim rests on the report's own runs and on the mechanism. ## Scope and preconditions - The other user can create files in the lock's directory; a root-owned `1777` directory is enough. It creates, `utime`s (sets the timestamps of) and unlinks only its own file. In the sticky directory its `unlink`, `rename`, `open(..., "w")` and `utime` on a marker of the lock user all fail, and deleting its own file only frees the lock. - The directory is on a filesystem that reuses a freed inode number at once: ext4, and overlayfs on it (thousands of create/unlink cycles returned one number). tmpfs does not reuse one, so the pair cannot repeat there; btrfs does not either. - File timestamps come from a coarse clock whose future values the other user can compute (`CLOCK_REALTIME_COARSE`, 4 ms steps on Linux 6.1; 20,000 file creations all carried that clock's value). - At least two processes of the lock user are waiting on the lock while the other user's file is there. The other user does not choose which processes overlap or when; each attempt blocks the lock for 2.2 s (for a lease, `lease_duration`). - No symbolic link, hard link or FIFO takes part, so the `fs.protected_*` sysctls (Linux settings that restrict following links and opening pipes in sticky world-writable directories) play no part. `StrictSoftFileLock` keys its watch the same way but only raises `SoftFileLockProtocolError`, so it breaks no lock there. 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 watch is [`SoftFileLock._marker_unchanged_for`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b8d080ca8b673a538c1/src/filelock/_soft.py#L125-L132), with its caller in [`_try_break_stale_lock`](https://github.com/tox-dev/filelock/blob/b831496b0273ecd11bf02b | GCVE Database | ||
| Medium | 6.3 | EPSS 0.4% | Medium6.3EPSS 0.4%GCVE DatabaseCVE-2026-101911 Multiple security vulnerabilities have been identified and fixed in the Node.js 24 module for Red Hat Enterprise Linux 9. These include denial of service issues via unbounded IPv6 parsing and crafted brace patterns, out-of-bounds write in the Brotli decoder, TLS certificate hostname bypasses, DNS hostname truncation, predictable random values, permission and authentication bypasses, and response desynchronization. The update rebases Node.js 24 to the latest upstream release and addresses these security flaws. | GCVE Database | ||
| Medium | 6.9 | EPSS 0.2% | CVE-2026-110860GitPython · Submodule.move() follows an attacker-committed intermediate symlink outside the worktree Medium6.9EPSS 0.2%GCVE DatabaseCVE-2026-110860 ## Summary GitPython's public `Submodule.move()` API verifies that its destination is lexically inside the parent repository, but it does not reject symbolic links in the destination's intermediate components before filesystem operations. On systems that check out Git symlinks, an untrusted repository can therefore redirect a fixed, application-selected move destination outside the parent worktree. The reporter reproduced this on the latest release, GitPython 3.1.62, and current `main`. In the strict scenario, the repository commits `vendor -> ../outside`, while the consuming application independently passes the fixed literal `vendor/module`. `Submodule.move()` follows `vendor`, moves the initialized submodule checkout to `outside/module`, rewrites `.gitmodules` and the index to the symlinked path, and leaves the index unable to produce a tree. The same result occurs when the committed symlink contains an absolute path to an external directory. No attacker-controlled API argument, local symlink planting, race, Git hook, unsafe-option flag, or network access during the proof is required. ## Trust boundary and preconditions The attacker controls: - an untrusted Git repository that the application clones; - a mode-`120000` tracked symlink named `vendor`; - the initialized submodule's content. The consuming application controls the call and uses a fixed layout rule: ```python submodule.move("vendor/module") ``` The trigger requires the application to initialize the submodule and later call `Submodule.move()` with a path below the attacker-committed symlink. Ordinary clone or submodule update does not automatically make this call. Windows was not tested. The primitive applies where Git materializes the tracked entry as a real symlink; on Windows that generally requires `core.symlinks=true` with Developer Mode or sufficient privilege. Configurations that materialize it as an ordinary file do not take this path. ## Product and directly verified revisions - Package: `GitPython` (`pip` ecosystem) - Latest release: 3.1.62, tag commit `db47516dac750c5968b8f6d388b18712c355df50` - Current `main`: `b62e91be897f687a622e40cd1e59c33dce45f124` - Relevant `git/objects/submodule/base.py` SHA-256 in both revisions: `a4355d49e0b75c7c7e74d33ac70ff9550f940c977848c99da40eb41e034b189c` - Upstream state rechecked: 2026-09-10 KST Only these two revisions were directly tested. ## Severity and weakness The demonstrated impact is an integrity boundary violation: repository content selects an existing directory outside the worktree, and GitPython moves an attacker-influenced checkout into a missing or empty child path there with the victim process's permissions. It also removes the original checkout and corrupts the parent repository's index/configuration state. For a downstream service that remotely accepts repositories and performs this exact layout operation with user involvement retained, `AV:N` gives a conditional application-level score of 7.1. A fully automatic service may warrant a different `UI` assessment. The library report uses `AV:L` because the vulnerable API call is a separate application action and is not part of GitPython's ordinary clone/update flow. No confidentiality impact, code execution, overwrite of an existing file, or overwrite of a non-empty directory was demonstrated. ## Reproducer The following script creates only temporary local repositories and a self-generated canary. It passes `protocol.file.allow=always` separately with `-c` on each fixture Git invocation; that permission is process-scoped and is not persisted to global or repository configuration. Run it from a fresh source checkout: ```bash git clone --depth 1 --branch 3.1.62 https://github.com/gitpython-developers/GitPython.git python3 poc.py /path/to/GitPython python3 poc.py --absolute-link /path/to/GitPython ``` `--absolute-link` commits the temporary external directory's absolute path instead of `../outside`. ```python #!/usr/bin/env python3 """Local-only reproducer for GitPython Submodule.move() symlink traversal.""" from __future__ import annotations import argparse import json import subprocess import sys import tempfile from pathlib import Path from typing import Optional def run_git( *args: str, cwd: Optional[Path] = None, check: bool = True ) -> subprocess.CompletedProcess: command = [ "git", "-c", "user.name=GitPython security fixture", "-c", "user.email=fixture@example.invalid", "-c", "protocol.file.allow=always", *args, ] return subprocess.run(command, cwd=cwd, check=check, text=True, capture_output=True) def read_core_worktree(parent) -> str: module_config = Path(parent.git_dir) / "modules" / "logical-name" / "config" return parent.git.config("--file", str(module_config), "--get", "core.worktree") def read_gitmodules_path(parent) -> str: modules_file = Path(parent.working_tree_dir) / ".gitmodules" return parent.git. | GCVE Database | ||
| High | 8.7 | EPSS 0.4% | CVE-2026-110859pygitup · GitPython: arbitrary file read via attacker-controlled branch name in `Head.checkout()` (incomplete fix of High8.7EPSS 0.4%GCVE DatabaseCVE-2026-110859 > **A note on the referenced files.** This report was submitted through GitHub's private > vulnerability reporting API, which carries no attachments. Every transcript this report leans > on is reproduced **inline above**, verbatim. The file names it mentions (`poc.sh`, `poc.log`, > per-case transcripts, `evidence.md`, `negative-controls.md`, blob-sweep tables) are the local > evidence bundle those inline excerpts were extracted from, with a checksum manifest. I am happy > to send the bundle by any channel you prefer, or to open a private fork — just say which. The > PoC scripts are self-contained and reproduce the result from a clean environment. --- **Affected:** `<= 3.1.62` — all published releases; also `main` @ `4905b4af` **Verified at runtime:** GitPython `3.1.62` with git `2.55.0` **Date:** 2026-09-10 **Reporter:** MAXI8594 --- ## Threat model | | | |---|---| | **Attacker** | anyone who can get the application to clone a URL they control. No account, no credential, no privilege in the consuming application. | | **Pre-need** | a repository they host, with `HEAD` pointing at a branch whose name is the payload. A `git push` to a plain git server is enough — no filesystem access to a server is required (`raw/06`). | | **Gets** | the contents of any file the application's process can read, returned in band through the exception. `/etc/passwd` recovered 34 of 34 lines. | | **Real-world likelihood** | **High.** The vulnerable consumer is six lines of ordinary code with no unusual configuration: `Repo.clone_from(user_url)` then `repo.active_branch.checkout()`. `allow_unsafe_options=False` is the default. Cloning user-supplied URLs is the library's central use case — CI systems, repository importers, scanners and bots. | **On severity relative to the sibling advisories.** GitHub, as CNA, scored the three prior advisories in this class at 6.5 / `PR:L` (CVE-2026-76217, CVE-2026-78678, CVE-2026-78679). This report asks for 7.5 / `PR:N`, and the difference is one measured fact: in those three, the application had to pass an attacker-influenced value into the API. Here it passes nothing — the clone materialises the hostile branch on its own and `repo.active_branch` returns it (`raw/02`). If that reading is not accepted, the `PR:L` path via `Repo.create_head()` is also real and lands at 6.5, matching the siblings. ## Summary `Head.checkout()` guards its `**kwargs` against unsafe Git options but never inspects its own positional argument — the branch name — which reaches the `git checkout` argv with no `--` separator. A repository whose default branch is named `--pathspec-from-file=/etc/passwd` makes a consumer that merely clones it and checks out the default branch return the entire contents of that file to the caller. This is the exact combination of two advisories the project already fixed: - **CVE-2026-76217** (`GHSA-hh9p-6wh2-4mfc`) — *"Arbitrary file read via `--pathspec-from-file` in `IndexFile.remove()` **and `Head.checkout()`**"*. Fixed in 3.1.58 by commit `f2550b65`, which added the guard to this exact method — but only for `**kwargs`. - **CVE-2026-78679** (`GHSA-3wxw-xv34-2frg`) — *"`TagReference.create` positional reference bypasses kwargs-only `--file` guard, enabling arbitrary file read (incomplete fix of 3af0c251)"*. Fixed in 3.1.59 by commit `1b0d2d9b`, which applied the positional check to `git/refs/tag.py`, `git/diff.py`, `git/index/base.py` and `git/repo/base.py` — but not to `git/refs/head.py:286`. `f2550b65` states in its own commit message that the audit found the positional variant and applied it to `HEAD.reset` — 185 lines above `Head.checkout`, in the same file, in the same commit: > "The audit also found that **reset's positional commit could carry the option before its argument > separator**. […] **Check reset's positional commit as well as keyword options**" `HEAD.reset` got the fix. `Head.checkout` did not, and `1b0d2d9b` did not come back for it. ## Affected Asset | | | |---|---| | Package | `GitPython` (PyPI) | | Affected versions | **`<= 3.1.62`** — all published releases | | Latest release tested | `3.1.62` | | Also present on | `main` @ `4905b4af` | | Vulnerable file | `git/refs/head.py` | | Guard | line 286 — `Git._option_candidates([], kwargs)` | | Sink | line 293 — `self.repo.git.checkout(self, **kwargs)` | | CWE | CWE-88 (Argument Injection), CWE-73 (External Control of File Name or Path), CWE-200 (Exposure of Sensitive Information) | | CVSS 3.1 | `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` → **7.5 High** | The version range was established by **blob hash against every tag**, not by dates: the sink line `self.repo.git.checkout(self, **kwargs)` — with no `--` separator — is present in `git/refs/head.py` in every tag from `0.3.1-beta2` through `3.1.62`. The `3.1.62` blob is byte-identical to `main`: `7f563374ef9e018b3d5f95054498e8591e112ce2`. Full sweep in `raw/07-blob-version-sweep.txt`. The kwargs-only guard first appears in **3.1.58**, so `3.1.58`–`3.1.62` i | GCVE Database | ||
| Critical | 9.2 | EPSS 0.5% | CVE-2026-110858Pygitup: GitPython Remote.pull() refspec bypasses the --upload-pack unsafe-option guard, enabling arbitrary command Critical9.2EPSS 0.5%GCVE DatabaseCVE-2026-110858 ## Summary `Remote.pull()` in GitPython forwards its positional `refspec` argument directly to the `git pull` child process while its unsafe-option guard inspects only keyword arguments. An application that passes attacker-influenced data as the `refspec` (for example a user-supplied branch or refspec parameter in a web or CI service) can supply an option-shaped value such as `--upload-pack=<command>`. The value bypasses the `unsafe_git_pull_options` check, is placed after the `--` separator on the `git pull` command line, and is executed as the upload-pack program for local-path or SSH transports. This yields arbitrary command execution with the privileges of the calling process using the documented default arguments (`allow_unsafe_options=False`). ## Root Cause `Remote.pull()` is documented to reject dangerous options unless `allow_unsafe_options=True`, and `--upload-pack` is explicitly listed as such an option. The guard, however, only screens keyword arguments and never the positional `refspec`. The check builds its candidate list with `Git._option_candidates([], kwargs)`, passing an empty list in place of the positionals, so the `refspec` value is never compared against `unsafe_git_pull_options = ["--upload-pack"]`. The normalized `refspec` is then forwarded verbatim to the `git pull` invocation. The leading `--` separator that neutralizes an option-shaped refspec for `git fetch` and `git push` does not protect `git pull`, because `git pull` re-forwards its trailing positionals to an internal `git fetch` without re-inserting a `--` separator, so the positional becomes an option to that internal fetch. ```text Affected function: git.remote.Remote.pull Source: git/remote.py (guard at the check_unsafe_options call using _option_candidates([], kwargs); sink at the self.repo.git.pull("--", self, refspec, ...) invocation) ``` ## Impact An attacker who controls the `refspec` value passed to `Remote.pull()` achieves arbitrary operating-system command execution as the process invoking GitPython, with no additional privileges and no user interaction, on the default code path. Exploitation requires that the pulled remote uses a transport that runs upload-pack on the local side, namely a local filesystem path or SSH; over HTTPS the `--upload-pack` value is inert because the server ignores a client-supplied upload-pack program. This transport requirement narrows the population of vulnerable deployments but does not add work for the attacker once a vulnerable target is reached. Working checkouts commonly use local-path or SSH origins, so the exposed population is substantial. ## Proof of Concept ```text Environment: GitPython from the affected source tree; system git 2.43.0. The pulled origin uses a local filesystem transport (protocol.file.allow=always for git >= 2.38). ``` Reproduction steps: 1. Create an origin repository reachable over a local-path transport. 2. In a consumer repository, register that origin and call `Remote.pull()` with an attacker-controlled refspec. 3. Observe that the injected command executes. ```python import git repo = git.Repo("consumer") # has an origin using a local-path or ssh transport origin = repo.remote("origin") # Attacker-controlled refspec value; default arguments (allow_unsafe_options=False): origin.pull(refspec="--upload-pack=touch /tmp/pwned; git-upload-pack") ``` ```text cmdline: git pull -v -- origin --upload-pack=touch /tmp/pwned; git-upload-pack [attack] marker created (RCE) : True [isolate] upload_pack passed as KWARG blocked : True (only the positional refspec escapes) [sibling] same value via git fetch : safe (pull-specific behavior) [control] benign refspec : safe ``` ## Attack Chain 1. **Exposure:** An application calls `Remote.pull(refspec=<value>)` on the default path, where `<value>` is derived from attacker-influenced input such as a branch or refspec request parameter. 2. **Control:** The attacker supplies the string `--upload-pack=<command>; git-upload-pack`. The value passes through `Git._unpack_args`, which only flattens and stringifies it, preserving the option shape. 3. **Path:** The protocol loop calls `Git.check_unsafe_protocols` on the value, which only matches remote-helper transport URLs (a `<transport>::` form) and does not flag an `--upload-pack=` token. 4. **Guard:** The unsafe-option guard calls `check_unsafe_options(options=Git._option_candidates([], kwargs), ...)`. Because positionals are passed as an empty list, the positional `refspec` is never inspected against `unsafe_git_pull_options`. 5. **Primitive:** The value is forwarded to `git pull -v -- <remote> --upload-pack=<command>; git-upload-pack`; `git pull` re-forwards the positional to an internal `git fetch` without a `--` separator, so it is honored as the upload-pack program. 6. **Result:** For a local-path or SSH transport, git executes `<command>` as the upload-pack program, giving arbitrary command execution as the calling p | GCVE Database | ||
| High | 7.7 | EPSS 0.4% | High7.7EPSS 0.4%GCVE DatabaseCVE-2026-110857 ### Summary GitPython has spent 2022-2026 rolling out two reusable guards method-by-method: `Git.check_unsafe_protocols(url)` (raises `UnsafeProtocolError` for `<transport>::` remote-helper URLs - CVE-2022-24439) and `Git.check_unsafe_options(...)` (raises `UnsafeOptionError` for denylisted option spellings - CVE-2023-40267, CVE-2026-42215, CVE-2026-67323). Three wrapper methods that build a `git` argv from caller-controlled values still have **neither**: 1. **`Git.ls_remote`** (git/cmd.py:1128-1143) - checks options (:1139-1141) but forwards the URL positional unchecked; no `allow_unsafe_protocols` parameter exists. The same URL through `clone_from` raises `UnsafeProtocolError`. 2. **`Repo.merge_base`** (git/repo/base.py:895-936) - forwards `**kwargs`/`*rev` to `self.git.merge_base` with no guard; guarded sibling `iter_commits` sits 12 lines up. Its exception handler additionally swallows every non-128 `GitCommandError` into `[]`, masking the injection. 3. **`IndexFile.move`** (git/index/base.py:1092-1168) - forwards `**kwargs` to `git mv` twice (:1147, :1167); guarded sibling `remove()` is 23 lines above. Verified unfixed on `main @ a54d159` (2026-09-21), post-dating latest release 3.1.62 (2026-09-07) - all three sites re-verified today. ### Impact **Site 1 (`ls_remote`)** - RCE conditional on the victim git transport policy. The common 'probe first, then clone' pattern (`Git(...).ls_remote(attacker_url)`) with an `ext::sh -c ...` URL executes arbitrary commands when the victim git permits the ext transport: dynamically verified with `protocol.ext.allow=always` (marker executed). git >= 2.38.1 stock config refuses the transport (`fatal: transport 'ext' not allowed`, rc 128) - GitPython adds no defense-in-depth and surfaces a bare `GitCommandError`. Environments on git < 2.38.1 are exposed with stock config - the exact population CVE-2022-24439 was scored against. **Sites 2/3** - latent argument-injection surface: caller kwargs flow 1:1 into the `git` argv before operands and `--` (cmd.py:1850 `opt_args + ext_args`), and `execute_kwargs` (incl. `shell=True`/`env` - the CVE-2023-40267 Windows mechanism) passes through unguarded. Today `git merge-base`/`git mv` expose no exec option, so we claim the missing-guard class (vendor-settled by CVE-2026-67323, which bundled archive/ls_remote/iter_commits/blame in one advisory), not direct RCE. ### Dynamic verification (today; GitPython main @ a54d159, git 2.55.0, Linux) | # | Call | Result | |---|---|---| | T1 | `repo.git.ls_remote('ext::/tmp/gp_touch.sh')`, stock config | git refuses (rc 128); **no `UnsafeProtocolError` from GitPython** | | T1b | same + victim config `protocol.ext.allow=always` | **marker created - arbitrary command executed** | | T2 | `repo.git.ls_remote('.', upload_pack=...)` | blocked by cmd.py:1141 (options-side fixed - protocol-side is the residual gap) | | T3 | `repo.merge_base('main','side', upload_pack=...)` | **not blocked**; argv `git merge-base --upload-pack=... main side`; rc 129 silently converted to `[]` | | T4 | `repo.index.move(['f1','f2'], output=...)` | **not blocked**; option reaches `git mv` (rc 129) | ### Coverage matrix (main @ a54d159) `check_unsafe_protocols`: 7 call sites (`_clone`, `archive`, `set_url`, `create`, `fetch`, `pull`, `push`) - none on `ls_remote`. `check_unsafe_options`: 24 call sites incl. `ls_remote`, `iter_commits`, `IndexFile.remove` - absent on `merge_base` and `move`. ### Precedents (same repo, same guard family) - **CVE-2022-24439 / GHSA-hcpj-qp55-gfph** (critical): `check_unsafe_protocols` on the clone path - Site 1 is the same `ext::` primitive on a sibling API. - **CVE-2023-40267 / GHSA-pr76-5cm5-w9cj** (critical, 'incomplete fix for CVE-2022-24439'): `check_unsafe_options` - unguarded kwargs to argv treated as RCE (`shell=True` on Windows). - **CVE-2026-42215 / GHSA-rpm5-65cw-6hj4** (high, fixed 3.1.47): underscored-spelling bypass - rollout still active in 2026. - **CVE-2026-67323 / GHSA-956x-8gvw-wg5v** (high, 8.4, fixed 3.1.51): added `check_unsafe_options` to `archive`/`ls_remote`/`iter_commits`/`blame`; its PoC is literally `repo.git.ls_remote('.', upload_pack=...)`. Establishes that a wrapper missing the check is itself a vulnerability. This report is the residual gap of that rollout: `ls_remote` got options but not protocols; `merge_base`/`move` got neither. ### Recommended fix (existing helpers, 1-3 lines per site) 1. `Git.ls_remote`: `if not allow_unsafe_protocols: self.check_unsafe_protocols(args[0])` + new `allow_unsafe_protocols: bool = False` (the `_clone` pattern, base.py:1536-1538). 2. `Repo.merge_base`: `check_unsafe_options(Git._option_candidates(list(rev), kwargs), unsafe_options=self.unsafe_git_revision_options)` (the `iter_commits` pattern); stop masking non-128 statuses. 3. `IndexFile.move`: the `remove()` pattern with `unsafe_git_pathspec_from_file_options`. One advisory, three affected methods, severity anchored on Site 1 - mirroring CVE-2026-67323's bundling. | GCVE Database | ||
| High | 8.7 | EPSS 0.3% | CVE-2026-110856Pygitup: GitPython parse_actor_and_date() is vulnerable to quadratic backtracking on malformed identity lines High8.7EPSS 0.3%GCVE DatabaseCVE-2026-110856 ### Summary This is a residual of GHSA-g5vv-9gxw-82hx / CVE-2026-87819. The 3.1.60 fix made `Actor._from_string` regex-free, but `parse_actor_and_date` still runs `_re_actor_epoch` on the same author/committer/tagger line one step earlier. That pattern backtracks quadratically when the trailing epoch or timezone is missing or malformed, so the same class of slow parse is still reachable in 3.1.62. ### Details `git/objects/util.py:322`: ```python _re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$") ``` `.+?` and `(.*)` can both absorb the same run of spaces, and the required ` (\d+) ([+-]\d+)` tail is only checked after them. On a long line with no valid tail, the search retries every split point. It is called from `parse_actor_and_date` (`git/objects/util.py:335`), which runs on the author and committer lines in `Commit._deserialize` (`git/objects/commit.py:917`, `:931`) and on the tagger line in `git/objects/tag.py:129`. The published payload (letters plus an unterminated `<`) is fast on 3.1.62. Replacing the letters with spaces is not: | author line (after `author `) | 2 KB | 8 KB | 32 KB | |---|---|---|---| | `'a'*n + ' <'` (advisory payload) | 0.0 ms | 0.1 ms | 0.4 ms | | `' '*n` | 28 ms | 453 ms | 7,700 ms | | valid line, long name (control) | 0.0 ms | 0.0 ms | 0.0 ms | Cost grows about 16x for each 4x increase in length. ### PoC ```python import time from git.objects.util import parse_actor_and_date line = "author " + " " * 32000 t = time.perf_counter() parse_actor_and_date(line) print(f"{time.perf_counter() - t:.1f} s") # ~7 s on 3.1.62 ``` End to end, reading `Commit.author` on a commit object whose author line is 32 KB of spaces followed by any malformed tail (no epoch, no timezone, non-numeric epoch, or bad timezone) takes about 7.2 s per access. The same line with a valid `1191999972 -0700` tail takes 1.3 ms. Note for reproducing: `git hash-object -t commit -w` refuses such an object (`missingEmail`), so the test object was written with `--literally`. A remote can still serve one, because `fetch.fsckObjects` and `transfer.fsckObjects` default to false. This is the same delivery model as CVE-2026-87819 ("craft a commit object with a malformed author field"). ### Impact CPU exhaustion (CWE-1333) in any application that uses GitPython to read commits or tags from an untrusted repository: CI tooling, repository scanners, code-hosting integrations. Each access to `Commit.author`, `Commit.committer` or `TagObject.tagger` on a crafted object costs seconds, and a repository can contain many such objects. ### Suggested fix Match the leading field name with `.[^ ]*` instead of `.+?`. Both stop at the first space after the first character, but `.[^ ]*` cannot overlap the following `(.*)`, so the backtracking disappears: ```python _re_actor_epoch = re.compile(r"^.[^ ]* (.*) (\d+) ([+-]\d+).*$") _re_only_actor = re.compile(r"^.[^ ]* (.*)$") ``` Verification on current main (73a72c9): - Differential over 368 actor lines (real-world shapes plus edge cases): 0 differences in match groups for either regex. - `' '*n` at 64 KB: 32,170 ms before, 0.98 ms after. Valid lines are unchanged (0.01 ms). - The existing test suite gives the same pass/fail set with and without the change - The new regression test, `test_parse_actor_and_date_malformed_line_is_linear`, fails on the old pattern (8.4 s) and passes with the fix (0.5 s). The full patch (fix plus regression test) is attached below. ```diff diff --git a/git/objects/util.py b/git/objects/util.py index a68d701f..d8c0594d 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -319,8 +319,11 @@ def parse_date(string_date: Union[str, datetime]) -> Tuple[int, int]: # Precompiled regexes -_re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$") -_re_only_actor = re.compile(r"^.+? (.*)$") +# The leading field name ("author", "committer", "tagger") is matched with ``.[^ ]*`` rather than +# ``.+?``: both stop at the first space after the first character, but ``.[^ ]*`` cannot overlap the +# following ``(.*)``, so a line with no valid trailing epoch/offset no longer backtracks quadratically. +_re_actor_epoch = re.compile(r"^.[^ ]* (.*) (\d+) ([+-]\d+).*$") +_re_only_actor = re.compile(r"^.[^ ]* (.*)$") def parse_actor_and_date(line: str) -> Tuple[Actor, int, int]: diff --git a/test/test_util.py b/test/test_util.py index cf4299d5..e1631420 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -22,6 +22,7 @@ from git.cmd import dashify from git.objects.util import ( altz_to_utctz_str, from_timestamp, + parse_actor_and_date, parse_date, tzoffset, utctz_to_altz, @@ -571,6 +572,30 @@ class TestUtils(TestBase): Actor("name last another", "some-very-long-email@example.com"), ) + def test_parse_actor_and_date_malformed_line_is_linear(self): + # A long author line whose trailing epoch/offset is missing or malformed used to make + # _re_actor_epoch backtrack | GCVE Database | ||
| High | 7.1 | EPSS 0.3% | High7.1EPSS 0.3%GCVE DatabaseCVE-2026-110855 ### Summary `Repo._clone()`'s handling of `separate_git_dir` never disables `Git.polish_url()`'s environment-variable expansion, unlike every other caller of that helper that handles attacker-influenced data. Since `separate_git_dir` can be derived from an attacker-controlled `.gitmodules` submodule name, a completely ordinary `clone` + `submodule_update(init=True, recursive=True)` on a malicious repository expands an environment variable of the attacker's choosing and exposes its value both as a persistent on-disk directory name and, whenever DEBUG-level logging is enabled (a common CI/verbose-logging configuration), in a `Popen(...)` log line that is not redacted the way credential-bearing arguments are. ### Root cause / Details `git/repo/base.py`'s `_clone()` has: ```python sep_dir = kwargs.get("separate_git_dir") if sep_dir: kwargs["separate_git_dir"] = Git.polish_url(sep_dir) ``` This is the only remaining `Git.polish_url()` call site handling attacker-influenced data that does not pass `expand_vars=False` (there is one other unguarded call site, `clone_path` at line 1537, but its input is the caller's own local destination path, not attacker-controlled). The sibling fixes for `GHSA-94p4-4cq8-9g67`/`GHSA-rwj8-pgh3-r573` added `expand_vars=False` to `clone_url` (5 lines below this one, in the same function), `git/repo/base.py:1599`, `remote.py:814`, and `submodule/base.py:686` — this one did not receive it. (`submodule/root.py` also currently sets `expand_vars=False` at a related call site, but that was added incidentally by a later, unrelated commit, not as part of these security fixes.) `Git.polish_url()`'s docstring itself says: "Disable this for values obtained from an untrusted source, such as remote URLs." Its non-Cygwin branch does plain `os.path.expandvars()` on the entire string, including embedded variables anywhere in an absolute path. `separate_git_dir` reaches `_clone()` from `Submodule._clone_repo()` (`git/objects/submodule/base.py`) as `module_abspath = osp.join(parent_repo.git_dir, "modules", name)`, where `name` is the raw `.gitmodules` submodule section name — fully attacker-controlled content of a cloned repository. `_validated_name()` (added by the fix for `GHSA-hmq2-w58f-27jc`) only rejects names that are absolute, contain a drive letter, or contain `..` path components; it does not reject `$`. `_need_gitfile_submodules()` is true for git ≥ 1.7.5, i.e. the default path for virtually every real-world installation. ### Reproduction (live, against real, unmodified GitPython code, no monkeypatching) Built a hostile repository with a plain, ordinary git command: `git submodule add --name '$GPY_SECRET_MARKER' ../evil_submodule sub`. Set an environment variable in the "victim" process (standing in for a real CI secret such as `AWS_SECRET_ACCESS_KEY`): `GPY_SECRET_MARKER=AKIA_TOTALLY_FAKE_BUT_SECRET_LOOKING_VALUE_12345`. Then ran the exact ordinary workflow the `GHSA-hmq2-w58f-27jc` advisory's own PoC also uses: ```python repo = Repo.clone_from(evil_parent_url, victim_clone_dir) repo.submodule_update(init=True, recursive=True) ``` Observed result (`ls .git/modules/`): ``` .git/modules/AKIA_TOTALLY_FAKE_BUT_SECRET_LOOKING_VALUE_12345/ (a full git repo: HEAD, config, objects/, refs/, hooks/) ``` The literal `$GPY_SECRET_MARKER`-named directory was never created — only the expanded one exists, proving expansion happened before the value reached the `git clone --separate-git-dir=...` subprocess argument. The persistent, expanded-name directory under `.git/modules/` is itself the primary disclosure channel — it survives on disk for as long as the clone does. A second, independent channel: with the `git.cmd`/`git.repo.base` loggers at DEBUG level (a common CI/verbose-logging setting, not requiring any special flag), GitPython's own `Popen(...)` debug log line prints the fully expanded `--separate-git-dir=<value>` subprocess argument verbatim — `remove_password_if_present()` only redacts URL passwords/Authorization headers, not path-style arguments like this one, so the expanded value is not scrubbed. (The call also fails downstream with an `OSError`/`FileNotFoundError` referencing the original, unexpanded name, since `_write_git_file_and_module_config()` looks for a directory using the pre-expansion name — but that error message itself does not contain the expanded/secret value.) **Control**: the same call with an ordinary (no `$`) submodule name succeeds normally and creates `.git/modules/<name>` as expected — isolating the gap to submodule names containing environment-variable syntax specifically. ### Already-known check Checked all 35 published `gitpython-developers/GitPython` security advisories in full text. `GHSA-hmq2-w58f-27jc` covers the same `name` → `.git/modules/<name>` path but for path-traversal (`../../..`), not variable expansion. `GHSA-59cr-6r3x-644w` covers the sibling `path` field reaching unguarded `os.makedirs()`. `GHSA-94p4-4cq8-9g67`/`GHSA-rwj8-pgh3-r573` co | GCVE Database | ||
| Critical | 9.2 | KEVEPSS 82% | Critical9.2KEVEPSS 82%GCVE Database A buffer overflow vulnerability in the Brotli decoder (c/dec/decode.c) in Google Brotli versions 1.0.3 through 1.2.0 on all platforms allows a remote unauthenticated attacker to corrupt adjacent decoder state memory, disclose sensitive memory contents, cause a denial of service, or potentially execute arbitrary code using a crafted Brotli-compressed stream. Users need to update any Brotli libraries or binaries between 1.0.3 through 1.2.0 to the latest available version: 1.0.10 or 1.1.1 or 1.2.1. | GCVE Database |
Page 36 of 9,555 · 7 duplicate CVE records merged