Nov 9, 2026·12 min read·2 visits
Versions of isomorphic-git prior to 1.38.6 fail to detect circular symbolic references during ref resolution in GitRefManager.resolve(). Cloning or fetching from a malicious Git remote that advertises mutually referential symref capabilities writes circular reference files to disk, causing subsequent Git operations (checkout, log, status, branch) to hang indefinitely.
CVE-2026-63475 (GHSA-96wc-2m84-2cj7) is an uncontrolled recursion and infinite loop vulnerability in isomorphic-git prior to version 1.38.6. When processing Git protocol responses from a malicious or man-in-the-middle (MitM) server, isomorphic-git writes server-advertised symbolic reference (symref) capabilities directly to the local repository filesystem without validating for circular dependencies. Because the reference resolution logic in GitRefManager.resolve() lacks both cycle detection and a default recursion depth limit, any subsequent operation that resolves the circular references enters a non-terminating asynchronous recursion loop. Because the malicious references are persisted on disk, this condition induces a persistent denial of service (DoS) against the affected repository.
isomorphic-git is a pure JavaScript implementation of the Git version control system designed to run in both Node.js and browser environments. It provides a programmatic API for executing standard Git operations—such as clone, fetch, checkout, log, status, and branch—by interacting directly with a pluggable filesystem interface (fs) and parsing Git protocol packets over HTTP or SSH transports. Because it operates entirely in user-space JavaScript without invoking an external C-Git binary, it is commonly embedded in browser-based integrated development environments (IDEs), static site generators, and automated continuous integration (CI) pipelines.
A crucial part of the Git wire protocol occurs during the initial reference discovery phase (e.g., git-upload-pack), where a remote server advertises its available object references alongside protocol capabilities. Among these capabilities is symref=<ref>:<target>, which notifies the client that a particular reference is a symbolic pointer to another reference rather than pointing directly to a 40-character hexadecimal SHA-1 commit object ID (OID). During a clone or fetch operation, isomorphic-git parses these server-supplied symref capabilities and persists them as loose reference files inside the local .git/refs/remotes/ directory via its internal updateRemoteRefs workflow.
In versions of isomorphic-git prior to 1.38.6, the internal reference manager (GitRefManager.resolve()) does not enforce a default maximum recursion depth or track visited references when traversing symbolic reference chains. This flaw is classified under CWE-674 (Uncontrolled Recursion) and CWE-835 (Loop with Unreachable Exit Condition). When an application clones or fetches from an untrusted or compromised remote repository that advertises circular symbolic references, those circular definitions are committed directly to the local filesystem.
Once these mutually referential symbolic pointers exist on disk, any subsequent API call that attempts to resolve the affected remote references triggers an endless chain of asynchronous recursive calls inside GitRefManager.resolve(). Because each recursive step awaits an asynchronous filesystem or packed-ref lookup, the JavaScript call stack does not immediately overflow; instead, the promise chain never settles, resulting in continuous event-loop activity and a permanent application hang whenever the repository is accessed.
In the standard Git specification, a symbolic reference is a plain-text file located within the .git/ directory structure containing a string formatted as ref: <target-ref>\n. Resolving a symbolic reference requires reading the file, extracting the target reference path, and recursively looking up the target until a 40-character hexadecimal SHA-1 object identifier is reached. To prevent circular pointer chains from causing infinite loops, the canonical C implementation of Git enforces a strict hardcoded traversal limit defined by the constant SYMREF_MAXDEPTH (set to 5). If a resolution chain exceeds five hops, C-Git aborts the lookup and returns an error.
In isomorphic-git, reference resolution is handled by the static asynchronous method GitRefManager.resolve({ fs, gitdir, ref, depth = undefined }) located in src/managers/GitRefManager.js. While the function signature includes an optional depth parameter designed to halt recursion when depth decrements to -1, the default value of depth is undefined. Throughout the isomorphic-git codebase, internal callers invoked by high-level commands (git.log, git.status, git.checkout, git.branch) call GitRefManager.resolve() without specifying a depth argument.
When depth is undefined, the boundary check if (depth !== undefined) inside GitRefManager.resolve() evaluates to false on every invocation. When the method reads a loose reference file from disk (or from the packed-refs file) whose content begins with ref: , it strips the ref: prefix and recursively calls GitRefManager.resolve() with the extracted target string and the unchanged depth: undefined parameter.
Furthermore, during remote synchronization, isomorphic-git accepts multiple symref= tokens from the server capability advertisement without checking whether the advertised targets form a cycle. If a remote server advertises symref=refs/remotes/origin/a:refs/remotes/origin/b and symref=refs/remotes/origin/b:refs/remotes/origin/a, both files are written to disk. When GitRefManager.resolve() is later asked to resolve refs/remotes/origin/a, it reads ref: refs/remotes/origin/b, recurses to read refs/remotes/origin/b, finds ref: refs/remotes/origin/a, and loops indefinitely between the two references.
Prior to version 1.38.6, GitRefManager.resolve() in src/managers/GitRefManager.js relied solely on the optional depth parameter to terminate recursive traversal. As shown in the vulnerable implementation below, when depth is omitted by the caller, no mechanism exists to detect whether a reference path has already been evaluated during the current resolution chain:
// Vulnerable implementation in src/managers/GitRefManager.js (< 1.38.6)
static async resolve({ fs, gitdir, ref, depth = undefined }) {
if (depth !== undefined) {
depth--
if (depth === -1) {
return ref
}
}
// Is it a ref pointer?
if (ref.startsWith('ref: ')) {
ref = ref.slice('ref: '.length)
// Passes depth (still undefined) without tracking visited targets
return GitRefManager.resolve({ fs, gitdir, ref, depth })
}
// Is it a complete and valid SHA?
if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
return ref
}
// We need to look it up in loose refs or packed-refs
const packedMap = await GitRefManager.packedRefs({ fs, gitdir })
let sha
// ... [reads loose file from `${gitdir}/${ref}` or packedMap] ...
if (sha) {
// Recursively resolves the file contents (e.g., 'ref: refs/remotes/origin/b')
return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
}
}In commit 823843d1cd19182df79bc907c767f5bcecb39a0b (merged via Pull Request #2370), the maintainers remediated the vulnerability by introducing explicit cycle detection via a visited Set object initialized in the method parameters:
diff --git a/src/managers/GitRefManager.js b/src/managers/GitRefManager.js
@@ -3,6 +3,7 @@ import '../typedefs.js'
// This is a convenience wrapper for reading and writing files in the 'refs' directory.
import AsyncLock from 'async-lock'
+import { InternalError } from '../errors/InternalError.js'
import { InvalidOidError } from '../errors/InvalidOidError.js'
import { NoRefspecError } from '../errors/NoRefspecError.js'
import { NotFoundError } from '../errors/NotFoundError.js'
@@ -255,7 +256,13 @@ export class GitRefManager {
* @param {number} [args.depth = undefined] - The maximum depth to resolve symbolic refs.
* @returns {Promise<string>} - The resolved object ID.
*/
- static async resolve({ fs, gitdir, ref, depth = undefined }) {
+ static async resolve({
+ fs,
+ gitdir,
+ ref,
+ depth = undefined,
+ visited = new Set(),
+ }) {
if (depth !== undefined) {
depth--
if (depth === -1) {
@@ -266,7 +273,7 @@ export class GitRefManager {
// Is it a ref pointer?
if (ref.startsWith('ref: ')) {
ref = ref.slice('ref: '.length)
- return GitRefManager.resolve({ fs, gitdir, ref, depth })
+ return GitRefManager.resolve({ fs, gitdir, ref, depth, visited })
}
// Is it a complete and valid SHA?
if (ref.length === 40 && /[0-9a-f]{40}/.test(ref)) {
@@ -285,7 +292,21 @@ export class GitRefManager {
packedMap.get(ref)
)
if (sha) {
- return GitRefManager.resolve({ fs, gitdir, ref: sha.trim(), depth })
+ // Guard against circular symbolic references (e.g. a -> b -> a). Without
+ // tracking visited refs this recursion would not terminate.
+ if (visited.has(ref)) {
+ throw new InternalError(
+ `Circular reference detected while resolving ref "${ref}"`
+ )
+ }
+ visited.add(ref)
+ return GitRefManager.resolve({
+ fs,
+ gitdir,
+ ref: sha.trim(),
+ depth,
+ visited,
+ })
}
}Whenever GitRefManager.resolve() looks up a reference ref on disk or in packed-refs and retrieves its value sha, it first checks if (visited.has(ref)). If the reference has already been encountered during the active traversal, the method immediately throws an InternalError indicating that a circular reference was detected. Otherwise, it adds ref to the visited set and passes the same visited instance down to the next recursive call, including across the intermediate ref.startsWith('ref: ') stripping step.
> [!NOTE]
> Fix Completeness Assessment: Tracking visited nodes in a Set completely eliminates infinite loops caused by circular symbolic reference chains of any length (e.g., a -> a, a -> b -> a, or a -> b -> c -> a). However, because depth still defaults to undefined, a maliciously constructed repository containing an extremely long, non-circular chain of symbolic references (thousands of linear hops r1 -> r2 -> ... -> rN) could still incur high filesystem I/O overhead before terminating, as a hard numeric limit like C-Git's SYMREF_MAXDEPTH = 5 is not enforced by default.
Exploiting CVE-2026-63475 requires an attacker to induce a victim application running isomorphic-git (< 1.38.6) to clone or fetch from a malicious Git remote server, or to intercept an unencrypted HTTP Git connection via a Man-in-the-Middle (MitM) position. No authentication privileges on the victim application are required, though user interaction or an automated webhook/pipeline trigger is necessary to initiate the outbound Git connection to the attacker-controlled URL.
During the git-upload-pack reference advertisement phase, the rogue Git server responds with pkt-line formatted capabilities that include mutually referential symref entries. For example, the server can include symref=refs/remotes/origin/a:refs/remotes/origin/b and symref=refs/remotes/origin/b:refs/remotes/origin/a in the NUL-delimited capability string attached to the first advertised ref line. When isomorphic-git processes this response via updateRemoteRefs, it writes both symbolic reference files directly into .git/refs/remotes/origin/.
Once the clone or fetch completes, any subsequent operation that resolves remote references or enumerates branch pointers will trigger the infinite loop. The official regression test added in Pull Request #2370 (__tests__/test-GitRefManager-symref-cycle.js) demonstrates how resolving a two-node cycle (refs/remotes/origin/a $\leftrightarrow$ refs/remotes/origin/b) causes GitRefManager.resolve() to hang indefinitely on unpatched versions:
/* eslint-env node, browser, jasmine, jest */
import { GitRefManager } from 'isomorphic-git/internal-apis'
import { makeFixture } from './__helpers__/FixtureFS.js'
describe('GitRefManager symref cycle handling', () => {
it('returns (throwing) instead of looping on a -> b -> a', async () => {
if (globalThis.jest) jest.useRealTimers()
const { fs, gitdir } = await makeFixture('test-GitRefManager-symref-cycle')
// Plant circular references directly in gitdir
await fs.write(
`${gitdir}/refs/remotes/origin/a`,
'ref: refs/remotes/origin/b\n'
)
await fs.write(
`${gitdir}/refs/remotes/origin/b`,
'ref: refs/remotes/origin/a\n'
)
// Attempting to resolve either reference
const result = await Promise.race([
GitRefManager.resolve({ fs, gitdir, ref: 'refs/remotes/origin/a' }).then(
value => ({ value }),
error => ({ error })
),
new Promise(resolve =>
setTimeout(() => resolve({ timedOut: true }), 5000)
),
])
// On unpatched versions (< 1.38.6), resolve() never settles,
// resulting in timedOut: true.
// On patched versions (>= 1.38.6), an InternalError is thrown:
expect(result.timedOut).toBeUndefined()
expect(result.error).toBeDefined()
expect(result.error.message).toMatch(/circular/i)
}, 15000)
})In a real-world application—such as a cloud IDE, documentation builder, or GitOps synchronization agent—this unresolvable promise blocks the execution workflow. Because each iteration performs asynchronous I/O against the underlying fs implementation (such as Node's fs.promises or browser IndexedDB via LightningFS), the loop continuously schedules microtasks and I/O callbacks without crashing the process with a synchronous RangeError: Maximum call stack size exceeded, silently starving the application.
The primary security impact of CVE-2026-63475 is a persistent denial of service (DoS) affecting availability. The vulnerability is rated 4.3 (Medium) under CVSS v3.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L). Confidentiality and integrity of existing repository objects are unaffected (C:N/I:N), as the flaw neither leaks memory/filesystem contents outside the repository nor allows arbitrary file overwrites outside the standard .git/refs/ hierarchy.
The distinguishing characteristic of this vulnerability is its persistence on disk. Unlike transient denial-of-service flaws that only consume resources while an active network connection is maintained, the circular symbolic references advertised by the remote server are written permanently into .git/refs/remotes/ (or .git/packed-refs). Consequently, restarting the Node.js server process, refreshing the user's browser tab, or rebooting the container does not recover the application.
Every time the application restarts and attempts to initialize the workspace by running git.status(), git.branch(), git.log(), or git.checkout() on the affected .git directory, the process re-enters the non-terminating resolution loop. The repository remains permanently inoperable until an administrator or user manually inspects the .git/refs/ directory structure and deletes the circular reference files.
As of November 2026, the Exploit Prediction Scoring System (EPSS) assigns CVE-2026-63475 a score of 0.00398 (0.40%), placing it in the 34.33rd percentile. The vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and CISA's Vulnrichment SSVC assessment classifies exploitation status as poc, automatable as no, and technical impact as partial.
To fully remediate CVE-2026-63475, upgrade isomorphic-git to version 1.38.6 or later. Version 1.38.6 includes commit 823843d1cd19182df79bc907c767f5bcecb39a0b, which implements cycle detection in GitRefManager.resolve() and throws a catchable InternalError (Circular reference detected while resolving ref ...) whenever a circular symbolic reference chain is encountered.
Update the package using your project's JavaScript package manager:
# Using npm
npm install isomorphic-git@^1.38.6
# Using Yarn
yarn upgrade isomorphic-git@^1.38.6
# Using pnpm
pnpm update isomorphic-git@^1.38.6> [!NOTE]
> Post-Upgrade Repository Cleanup: Upgrading isomorphic-git to 1.38.6 prevents the infinite hang by throwing an InternalError, but any repository that was previously poisoned by a malicious remote will still contain the circular files in .git/refs/remotes/ and will throw this error during ref resolution. To restore normal operation to an already-affected repository, inspect .git/refs/remotes/ and remove the circular loose reference files, or delete and re-clone the repository from a verified source.
If an immediate upgrade to version 1.38.6 is not feasible, organizations can apply the following temporary mitigations:
git.clone() and git.fetch() operations against trusted, authenticated Git hosting providers over encrypted TLS/SSH connections..git/refs/remotes/ before running workspace commands (status, checkout, log), verifying that any file starting with ref: does not form a cycle or point to another symbolic reference beyond a maximum depth of 5.isomorphic-git APIs directly via GitRefManager.resolve(), always pass an explicit numeric depth bound (e.g., GitRefManager.resolve({ fs, gitdir, ref, depth: 5 })). Note that high-level public APIs (git.checkout, git.log, git.status) do not expose the depth parameter to callers.CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L| Product | Affected Versions | Fixed Version |
|---|---|---|
isomorphic-git isomorphic-git | < 1.38.6 | 1.38.6 |
| Attribute | Detail |
|---|---|
| CVE / GHSA ID | CVE-2026-63475 / GHSA-96wc-2m84-2cj7 |
| Weakness Class | CWE-674 (Uncontrolled Recursion) / CWE-835 (Infinite Loop) |
| CVSS v3.1 Score | 4.3 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L) |
| EPSS Score | 0.40% (34.33rd percentile) |
| Affected Component | GitRefManager.resolve() & updateRemoteRefs in isomorphic-git < 1.38.6 |
| Primary Impact | Persistent Denial of Service (non-terminating async recursion on repository access) |
| Exploit & KEV Status | PoC Available (Unit Test) / Not Listed in CISA KEV |
The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack, or entering an non-terminating asynchronous execution chain.
CVE-2026-63470 (GHSA-h667-4jj2-cgv4) is a code injection vulnerability (CWE-94) in fastapi-code-generator prior to version 0.8.1. The tool generates FastAPI route handlers by directly interpolating the OpenAPI Operation.summary field into triple-double-quoted Python docstrings without escaping embedded triple quotes or backslashes. An attacker who supplies or modifies an OpenAPI specification can break out of the docstring literal and inject arbitrary Python expressions into the generated route handler body, resulting in remote code execution when the affected HTTP endpoint is called.
CVE-2026-16454 is an object-level authorization bypass vulnerability (CWE-284, CWE-862) in Eclipse hawkBit versions 1.0.3 and prior. The flaw exists within the Direct Device Integration (DDI) REST controller (hawkbit-ddi-resource), where endpoints responsible for enumerating software module metadata and serving firmware binaries fail to verify whether the requested software module is assigned to the authenticated device. Consequently, any device with valid credentials (such as a TargetToken or GatewayToken) can enumerate software module identifiers and download arbitrary firmware artifacts across the entire tenant namespace.
CVE-2026-62284 (GHSA-5w5g-j6hw-6p8f) is a code injection and server-side request forgery vulnerability in the console-proxy component of the OSAC Operator. The RemoteConfigResolver parses Kubernetes Secrets containing kubeconfig files via clientcmd.RESTConfigFromKubeConfig without sanitizing execution-related fields. An attacker with RBAC permissions to create or modify labeled Secrets in monitored namespaces can supply a malicious exec credential plugin, resulting in arbitrary command execution within the console-proxy pod.
CVE-2026-62361 (GHSA-xgjr-7j9q-2h4r) is an authenticated SQL injection and authorization bypass vulnerability in listmonk, an open-source, self-hosted newsletter and mailing list manager written in Go. In versions prior to 6.2.0, the subscriber export endpoint (GET /api/subscribers/export) directly interpolates user-supplied SQL filter expressions into a PostgreSQL query without invoking the table allowlist validation function (validateQueryTables). Consequently, authenticated users possessing the subscribers:sql_query and subscribers:get_all permissions can bypass relational access controls, execute arbitrary subqueries against restricted tables such as users and settings, and modify database state via writable Common Table Expressions (CTEs).
CVE-2026-28385 (GHSA-3gq2-x4qg-p4g6) is a Server-Side Request Forgery (SSRF) vulnerability in Canonical LXD versions 4.12 through 6.9. An authenticated user possessing the can_create_images entitlement can force the LXD daemon to execute arbitrary outbound HTTP and HTTPS requests via the /1.0/images API endpoint. Due to the absence of egress destination validation, an attacker can probe host loopback services, RFC 1918 private network segments, and cloud Instance Metadata Services (IMDS) from the network position of the privileged LXD daemon.
Canonical LXD and LXC Incus contain an arbitrary file write vulnerability (CWE-73 / CWE-59) in the `/1.0/instances/<name>/exec` REST API endpoint. When an instance is unpacked from an image archive, top-level symbolic links are extracted directly onto the host filesystem within the instance storage directory. If a crafted image includes a symbolic link named `exec-output` pointing to an arbitrary host path, invoking the execution endpoint with `record-output: true` causes the privileged host daemon to follow the symlink and write command output files directly to the target host directory. An authenticated attacker with permissions to import images and execute commands can exploit this flaw to write controlled content to sensitive host directories such as `/etc/cron.d`, resulting in arbitrary code execution on the host as `root`.