CVE-2026-100689 is a path traversal vulnerability in GitPython 3.1.61 and main branch affecting the handling of the .gitmodules 'path' field during submodule updates. The vulnerability arises because the 'path' field is used without proper containment checks, allowing an attacker to specify a path outside the repository directory. This can lead to directory creation and file writes outside the intended repository location. The issue is mitigated in common workflows since Git prevents checkout of trees with '..' components, but it can be triggered when updating submodules at non-HEAD commits via the API. A fix is available in GitPython version 4.0.4_9 that applies the existing containment guard to the 'path' field.
Tornado versions before 6.5.9 have a vulnerability in CurlAsyncHTTPClient where highly compressed gzip responses can cause unbounded memory accumulation. This can lead to denial of service by exhausting system memory and crashing the application process.
PyJWT versions 2.11.0 through 2.13.0 contain a vulnerability where the decode() and decode_complete() functions mutate a caller-supplied options dictionary when signature verification is disabled. This mutation can cause subsequent calls that enable signature verification to inherit disabled claim checks such as expiration, audience, issuer, and subject verification, allowing acceptance of tokens with invalid claims without raising exceptions. This is a claims verification bypass, not a signature verification bypass. The issue is a regression introduced in March 2025 by a refactor that removed a shallow copy of the options dictionary. The vulnerability requires the application to reuse the same mutable options dictionary across decode calls with different verification intents.
GitPython versions prior to 1.2.0.post0_15 contain a Regular Expression Denial of Service (ReDoS) vulnerability in the Actor.name_email_regex pattern. This occurs when parsing commit author or committer headers containing a long string with an unterminated '<' character, causing catastrophic backtracking in the regex engine and resulting in CPU exhaustion proportional to the square of the input length. This can block GitPython API calls that access commit author or committer metadata for extended periods, impacting CI runners, code-hosting backends, and repository scanning tools that process untrusted commits.
GitPython version 3.1.59 contains a vulnerability where the repository's git directory is misresolved, allowing attacker-controlled repository content to impersonate the .git directory. This leads to arbitrary code execution when a victim performs a commit using GitPython's native index.commit() method. The vulnerability also allows reading sensitive files outside the repository and writing config files to attacker-chosen locations. The issue arises because GitPython tests candidate git directory paths in an order that considers the real .git directory last, causing it to treat the working tree root as the git directory if certain tracked files exist. This flaw affects services that open or clone untrusted repositories using GitPython. A fix is available in version 1.2.0.post0_15.
GitPython versions up to 1.2.0.post0_15 contain a vulnerability in the TagReference.create() method where a positional 'reference' argument can bypass the unsafe option guard for '--file'. This allows an attacker controlling the 'reference' parameter to cause an arbitrary local file read, with the file contents returned as the annotated tag message. The issue is an incomplete fix of a prior vulnerability and requires the embedding application to forward a user-controlled 'reference' value. A fix is available in version 1.2.0.post0_15.
## Impact
urllib3's [streaming API](https://urllib3.readthedocs.io/en/2.7.0/advanced-usage.html#streaming-and-i-o) is designed for efficiently handling large HTTP responses by reading the content in chunks, rather than loading the entire response body into memory at once. When decoding a [chunked-transfer-encoded response](https://httpwg.org/specs/rfc9112.html#chunked.encoding), this API reads each chunk's size field by buffering until it sees `\n` or EOF.
A malicious HTTP server can return `Transfer-Encoding: chunked` and then send a very long run of bytes without any newline, causing the streaming client to buffer that entire run before urllib3 can reject the chunk size as invalid and allocate more memory than intended.
To fix the issue, we'll reject chunk-size fields larger than 65536 bytes, as already done in the non-streaming case, which is currently handled by the Python standard library. Note that this only applies to reading the chunk-size field, not the chunk data, which is already handled correctly. Thus, there shouldn't be any impact for non-malicious servers.
## Affected usages
Applications and libraries using urllib3 versions earlier than 2.8.0 may be affected when streaming a chunked response from untrusted sources. Specifically, this affects the [read_chunked()](https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html#urllib3.response.BaseHTTPResponse.read_chunked) and [stream()](https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html#urllib3.response.BaseHTTPResponse.stream) methods of the HTTPResponse object.
This also affects requests streaming API, which uses urllib3 under the hood.
## Remediation
Upgrade to urllib3 version 2.8.0 or later, where chunk-size fields larger than 65536 will be rejected. If upgrading is not immediately possible, consider reading the response at once using the [read()](https://urllib3.readthedocs.io/en/stable/reference/urllib3.response.html#urllib3.response.BaseHTTPResponse.read) method.
### Impact
urllib3's streaming API is designed for the efficient handling of large HTTP responses by reading content in chunks instead of loading the entire response body into memory at once.
urllib3 can decompress response bodies according to the HTTP `Content-Encoding` header. When streaming a compressed, chunked response, urllib3 first consumes data already buffered by the decoder before reading the next HTTP chunk.
However, urllib3 versions from 2.6.2 through 2.7.0 could enter an infinite loop when a chunked response contained bytes after the end of the Deflate stream. If the decompressed body exceeded the requested streaming chunk size, Python's zlib implementation could retain the trailing bytes as unconsumed input after reaching the end of the compressed stream. urllib3 would repeatedly attempt to decode those same bytes without making progress or reading more data from the network.
A malicious server could exploit this behavior to cause excessive CPU usage and prevent the affected request from completing on the client. Network read timeouts would not interrupt the loop because no further socket operation was required.
### Affected usages
Applications and libraries using urllib3 versions 2.6.2 through 2.7.0 may be affected when all of the following conditions are met:
1. Compressed responses from an untrusted source are streamed using `HTTPResponse.stream(amt=N)` or `HTTPResponse.read_chunked(amt=N)` with a positive, finite chunk size.
2. Content decoding is enabled.
3. The response uses both `Transfer-Encoding: chunked` and `Content-Encoding: deflate`.
4. The decoded body exceeds the requested chunk size and the encoded body contains bytes after the end of the Deflate stream.
`HTTPResponse.stream()` uses a finite chunk size by default and is therefore affected when the other conditions are met.
### Remediation
Upgrade to urllib3 2.8.0, in which the Deflate decoder stops accepting input after reaching the end of the compressed stream and no longer reports trailing bytes as data that can produce more decoded output.
If upgrading is not immediately possible, disable automatic content decoding for responses from untrusted sources by setting `decode_content=False`, or reject streamed responses using the Deflate content encoding. Applications that disable automatic decoding must handle the compressed response safely at another layer.
`download_wheel()` runs `pip download` and hands the result straight to the seeder, with nothing checking the bytes it gets back. The embedded pip and setuptools wheels carry a `BUNDLE_SHA256` that virtualenv checks on every load, but a wheel fetched over the network for the periodic-update feature or the `--download` flag had no equivalent verification.
A compromised index, a stale mirror, or a MITM'd download (when TLS is intercepted, e.g. via a misconfigured or malicious CA) could substitute a different wheel under the same distribution/version/filename, and virtualenv would cache and seed it into every environment created afterward with no warning.
Fixed by computing the downloaded wheel's sha256 and comparing it against the digest PyPI's public JSON API reports for that exact release, independent of which index actually served the file. The check is skipped, not treated as failure, when a custom index (`PIP_INDEX_URL`, `PIP_EXTRA_INDEX_URL`, `PIP_INDEX`) is configured, since a private index can legitimately serve a different, intentionally rebuilt wheel. Fix: https://github.com/pypa/virtualenv/pull/3251
### Impact
The generated `activate` (bash/zsh) and `activate.fish` scripts interpolate a `shlex.quote`-ed value into a position that quotes it a second time. The extra quotes terminate the quoted run early and leave part of the value parsed as shell code, so a path containing shell metacharacters runs commands when a user sources the activation script.
`activate` is affected through the virtual environment's own path, on the branch that reports a relocated environment:
```sh
echo "Virtual environment directory __VIRTUAL_ENV__ does not exist!" >&2
```
For a destination named `x'$(id)'y`, `shlex.quote` emits `'x'"'"'$(id)'"'"'y'`. The inner `"` closes the enclosing double quote and `$(id)` is left unquoted. Backtick and `;` payloads reach the same result. This branch runs whenever the recorded path is absent, which is the normal case for a virtual environment copied or distributed to another machine.
`activate.fish` is affected through the interpreter's Tcl/Tk library paths:
```fish
set -gx TCL_LIBRARY '__TCL_LIBRARY__'
```
`shlex.quote` already returns `'/tcl/(cmd)/lib'`, so the rendered line is `''/tcl/(cmd)/lib''`. fish concatenates the adjacent quoted runs and expands the `(cmd)` left between them. The same doubling also splits a path containing a space into two list elements, corrupting `TCL_LIBRARY` and `TK_LIBRARY`.
This is the same defect class as GHSA-x78j-v8h9-3j2q, which covered `activate.bat`.
### Patches
Fixed in 21.7.13 by moving the placeholders outside the surrounding quotes, so each value keeps only the quoting `shlex.quote` applied. Fix: https://github.com/pypa/virtualenv/pull/3252
### Workarounds
Avoid creating or distributing virtual environments whose path contains `'`, `` ` ``, `$`, `;`, `(` or `)`, and inspect the generated activation script before sourcing one you did not create.
### Summary
`pyvenv.cfg` is a line-based format with no escape syntax. `PyEnvCfg.write()` wrote values verbatim, while `PyEnvCfg._read_values()` parses the file with `str.splitlines()`. A value containing a line boundary therefore became additional configuration lines, and because reading is last-wins, the injected keys replaced any key written earlier in the file.
### Impact
The `prompt` value is the reachable input: it is set by `--prompt`, by the `VIRTUALENV_PROMPT` environment variable, or from the config file, and `write()` emits `prompt` before `home`. A crafted prompt can therefore set `home` in the generated `pyvenv.cfg`:
```console
$ virtualenv --prompt $'x"\nhome = /attacker/path\nprompt = "z' venv
$ grep '^home' venv/pyvenv.cfg
home = /attacker/path
```
`home` is what tooling reads to locate the base interpreter, so a consumer that trusts it can be pointed elsewhere. `implementation`, `version_info`, `version`, `executable`, `command` and `virtualenv` are also written before `prompt` and can be replaced the same way.
This requires the prompt to come from somewhere other than the person running the command, for example a CI job templating a branch name into it, tooling deriving an environment name from user-supplied data, or an inherited `VIRTUALENV_PROMPT`. Where the operator supplies the prompt directly they already control the command line, and the effect is corruption rather than privilege gain: the value is truncated at the boundary and read back with a dangling quote.
### Details
The boundary set is the one `str.splitlines()` recognizes, which is wider than `\n`: `\r`, `\v`, `\f`, the file, group and record separators, `U+0085`, `U+2028` and `U+2029` were all written through unchanged and all split the line when read back.
### Patches
`PyEnvCfg.write()` now collapses those boundaries to spaces as it serializes each line, so it cannot emit a structurally invalid file regardless of what a caller places in `content`.
### Workarounds
Do not pass externally influenced data as the virtualenv prompt. Strip line boundaries from any value before using it as `--prompt` or `VIRTUALENV_PROMPT`.
## Summary
A timing side-channel vulnerability exists in the PKCE (RFC 7636) implementation
of the Authorization Code Grant flow. The `code_challenge_method_plain` function
uses Python's standard `==` operator for string comparison instead of a
constant-time comparison function, potentially allowing timing-based attacks.
## Affected Component
- File: `oauthlib/oauth2/rfc6749/grant_types/authorization_code.py`
- Functions: `code_challenge_method_plain`, `code_challenge_method_s256`
- Vulnerability Type: CWE-208 (Observable Timing Discrepancy)
## Technical Details
Python's `==` operator uses short-circuit evaluation when comparing strings:
1. Returns `False` immediately if lengths differ
2. Compares characters left-to-right, stopping at first mismatch
This means comparison time varies linearly with the length of the common prefix
between the attacker-supplied verifier and the stored challenge, creating a
measurable timing oracle.
## Proof of Concept
Tested locally against oauthlib source (network jitter eliminated to isolate
pure Python execution time):
| Input | Result | Time (10M iterations) |
|---|---|---|
| Wrong first char (`B` + `A`*49) | Fast reject | 0.34106s |
| 49 chars correct (`A`*49 + `B`) | Deep compare | 0.37847s |
| **Difference** | | **0.03741s** |
The ~37ms delta over 10M iterations corresponds to nanosecond-level differences
per call, which are statistically exploitable under controlled conditions.
## Attack Scenario
1. Attacker intercepts `authorization_code` via Custom URI Scheme Hijacking
2. PKCE blocks token request — attacker lacks `code_verifier`
3. Attacker sends repeated requests to `/token` endpoint measuring response times
4. Using timing oracle, attacker recovers `code_verifier` character by character
5. Attacker obtains Access Token → Account Takeover
> **Note:** Practical exploitability is limited due to the single-use nature of
> authorization codes and real-world network noise. However, the vulnerable
> pattern should be corrected as a defense-in-depth measure.
## Recommended Fix
Replace `==` with `hmac.compare_digest()` for constant-time comparison:
cr: Elvin Latifli
PyJWT versions from 2.1.0 up to but not including 2.15.0 contain a vulnerability in the OKPAlgorithm.from_jwk function where the private JWK import does not verify that the public key component x corresponds to the private key component d. This can lead to a mismatch between the identity derived from x and operations performed with d. If an integration accepts private key parameters from a proof header without rejecting them, an attacker could misuse a stolen sender-constrained token without possessing the legitimate private key. The issue is fixed in version 2.15.0.
PyJWT versions from 2.13.0 up to but not including 2.14.0 contain a vulnerability where deeply nested JSON arrays in the JWT header cause a RecursionError during parsing. This error is not caught by the library's usual exception handling, resulting in an HTTP 500 server error and a full traceback logged. This allows unauthenticated attackers to cause a denial of service on JWT validation endpoints without crashing the server or exposing data. The issue is fixed in PyJWT 2.14.0.
CVE-2026-102270 is a Regular Expression Denial of Service (ReDoS) vulnerability in the pyjwt library's is_pem_format function. The vulnerability arises from a regex pattern that exhibits quadratic time complexity when processing malformed PEM-like inputs consisting of repeated '-----BEGIN CERTIFICATE-----' lines without a corresponding '---- END' line. This can cause excessive CPU usage, potentially leading to resource exhaustion and denial of service. The issue affects pyjwt versions prior to 2.14.0. A fix replacing the vulnerable regex with a bounded marker scan was released in pyjwt 2.14.0 on 2026-09-11.
PyJWT versions prior to 2.14.0 accept JWS signature segments with non-canonical Base64URL characters, which do not affect the decoded signature or claims but alter the serialized token and its hash. This can allow bypass of logout or revocation mechanisms that rely on the raw serialized token as a security identity. The issue is not related to signature forgery and requires possession of a valid token. A fix rejecting such non-canonical characters is prepared but not yet released.
PyJWT versions prior to 2.14.0 contain a vulnerability in the is_pem_format function that improperly verifies cryptographic signatures. This flaw allows an attacker who knows a public key to forge authenticated HMAC tokens by exploiting how the library misinterprets certain malformed PEM public keys as HMAC secrets. The issue is resolved in version 2.14.0.
PyJWT versions prior to 2.14.0 contain a vulnerability in the PyJWKClient component where HTTP redirects during JWKS fetching are followed without validating the redirect destination. This can lead to exposure of sensitive request headers and allow an attacker-influenced redirect to poison JWKS trust, potentially causing acceptance of forged JWTs. The issue requires an attacker to influence the configured JWKS endpoint's response and is not triggered by token key IDs alone. The vulnerability is fixed in PyJWT 2.14.0.
PyJWT versions 2.9.0 through 2.13.0 contain a vulnerability where a malformed RSA JWK in a JWK Set causes the entire set parsing to abort due to an uncaught ValueError. This happens because the ValueError raised by the cryptography library during RSA key recovery is not caught by PyJWT's exception handling, leading to denial of service in key processing.