### 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
### 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
### Summary
The Secure attribute was not being enforced for the shared cookie.
### Impact
It may be possible to send a shared cookie marked Secure through an unencrypted request.
A critical vulnerability (CVE-2026-78676) in GitPython's GitConfigParser allows dormant multi-line git-config values to be corrupted into live injected directives upon any unrelated config write. This occurs because the parser's re-serialization logic mishandles multi-line values, causing injected directives like core.hooksPath to be interpreted by Git, enabling remote code execution (RCE). The issue affects GitPython version 3.1.58 at HEAD and specific kimi-cli versions. A patch is available.
A high-severity vulnerability in GitPython (CVE-2026-78675) allows arbitrary local file content disclosure via the [include] directive in an untrusted .gitmodules file. The SubmoduleConfigParser does not disable merge_includes, enabling an attacker to craft a malicious .gitmodules file that causes the parser to include and read arbitrary files on the victim's system. When the parser encounters a non-gitconfig file, it raises an exception that leaks the first line of the included file in the error message. This disclosure occurs during common read-only operations such as enumerating submodules, without requiring repository updates or checkouts. A fix is available for this vulnerability.
GitPython's Repo.blame() and Repo.blame_incremental() methods have an incomplete denylist for unsafe git revision options, omitting the --contents and -S options. This omission allows an attacker to cause arbitrary local file reads by passing these options, which echo file contents into the blame result. The vulnerability affects GitPython versions up to 3.1.58. A patch is available to address this issue by expanding the denylist to include these options.
Microsoft APM is an open-source, community-driven dependency manager for AI agents. Prior to 0.13.0, Microsoft APM contains a Windows-specific archive extraction boundary failure in the legacy-bundle probe used by apm install <bundle> on supported Python 3.10 and 3.11 runtimes. When apm install is given a local .tar.gz that is not recognized as a plugin-format bundle, APM probes whether it is a legacy --format apm bundle. On Python versions earlier than 3.12, that probe extracts untrusted tar members with raw tar.extractall() without rejecting Windows absolute member names such as D:/.... This vulnerability is fixed in 0.13.0.
Microsoft APM is an open-source, community-driven dependency manager for AI agents. Prior to 0.8.12, Microsoft APM normalizes marketplace plugins by copying plugin components referenced in plugin.json into .apm/. The manifest fields agents, skills, commands, and hooks are attacker-controlled, but the implementation does not enforce that those paths remain inside the plugin directory. A malicious plugin can therefore use absolute paths or ../ traversal paths to copy arbitrary readable host files or directories from the installer's machine during apm install. This vulnerability is fixed in 0.8.12.