Pollute one prototype, steal every Git token
CVE-2026-89011 · isomorphic-git · reported by izzysec
Asking a Git server which branches it has is about the most harmless thing a program can do. Nothing gets downloaded and nothing runs, so apps happily do it for any address a user pastes into an “import a repository” box, mostly just to fill in a dropdown. Nobody expects a list of branch names to be able to change how the rest of the app behaves.
In isomorphic-git, a JavaScript library that lets web pages and Node.js apps speak Git, it could. When the library turned a server’s branch list into a JavaScript object, a name starting with __proto__ didn’t create a branch at all, but changed the built-in template that every object in the app inherits from. That’s a bug class called prototype pollution, and in this case the attacker could use it to quietly add a proxy setting to everything. From then on, the app’s later Git requests that didn’t name a proxy of their own, including ones meant for GitHub or a company Git server, went through the attacker’s proxy, which only had to ask for a password to receive one. It was fixed in isomorphic-git 1.42.0 on September 10, 2026, and the CVE record and the GitHub advisory both rate it 7.1 High on CVSS 3.1.
What isomorphic-git actually is
Git normally means the git command-line program, which is written in C and has to be installed on the computer. That’s no use to a web page, which can only run JavaScript, and it’s awkward for Node.js apps that would rather not depend on whatever happens to be installed. isomorphic-git is Git rewritten in pure JavaScript, so the same code can clone, commit and push from a browser tab or a Node.js server (“isomorphic” is JavaScript jargon for code that runs in both places).
| What | Number | Source |
|---|---|---|
| GitHub stars | about 8.4k | GitHub |
| npm downloads, last week | about 3.31 million | npm |
| Of those, versions before 1.42.0 | about 64% | npm version stats |
Numbers as of November 2026. Downloads aren’t the same as installs, since build servers download the same thing over and over, but two out of three downloads going to old versions two months after the fix is still a lot.
It shows up in places you might not expect, such as the Obsidian Git plugin that syncs notes to a repository, the Git-backed content manager TinaCMS, and the scaffolder in Spotify’s developer portal Backstage, the part that creates new repositories from templates.
A short history of polluted prototypes
Prototype pollution, tricking code into changing a template that every JavaScript object quietly inherits from, is a JavaScript classic. The best-known examples are in lodash, the hugely popular utility library, whose merge functions could be tricked into it in 2018 and whose defaultsDeep was caught at it again in 2019. The usual cause is the same pattern behind this bug, code that takes names from outside and turns a/b/c-style paths into nested objects. isomorphic-git had a busy 2026, with six advisories between July and August about path traversal, symlinks and memory exhaustion, but this was its first prototype pollution.
How the attack works
A few terms, once:
- Ref: Git’s word for a named pointer to a commit. Branches and tags are both refs, with full names like
refs/heads/main. - getRemoteInfo: the isomorphic-git function that asks a server for its list of refs. The server answers with names and the commit IDs they point to.
- Prototype: in JavaScript, almost every object quietly inherits from one shared template object. If you ask an object for a setting it doesn’t have, JavaScript checks the template.
__proto__is a special name that leads to that template instead of to a normal property. - Prototype pollution: tricking code into writing onto that shared template, so a value appears as a default on every object in the program.
- CORS proxy: a relay server. Browsers block web pages from talking directly to most Git hosts, so browser apps send their Git traffic through a proxy, which isomorphic-git takes as an option called
corsProxy. - onAuth: a function the app gives isomorphic-git, which it calls when a server asks for a login. The app answers with the right username and token.
getRemoteInfo turns the server’s flat list of refs into a nested object, splitting each name on / and creating one level per part, so refs/heads/main becomes refs, then heads, then main. Its source comment explains this is because “99% of the time that will be easier to use”. The loop never checked the parts, so when a part was __proto__, it walked straight into the shared template and wrote the last part there as a setting, with the “commit ID” as its value. isomorphic-git didn’t check that the commit ID looked like one, so the server picks both the setting name and its value. This loop has been there since getRemoteInfo was added in 2018.
The setting worth picking is corsProxy. isomorphic-git reads its options in a way that also looks at the template, so after the pollution, every later network call in the same browser tab or Node.js process that didn’t set its own corsProxy gets the attacker’s proxy as a default. When that proxy replies “401, please log in”, isomorphic-git calls onAuth for the host the app actually meant to reach and sends the answer to the proxy. The pollution lasts until the tab is reloaded or the process restarts. The advisory has the full reproduction.
The diagram above is the whole attack. The only part it skips is how the attacker gets your app to look at their server in the first place, which can be as simple as a repository address pasted into the “connect a repository” or “import from URL” field of a web IDE, a CMS, a docs tool or an internal developer portal built on isomorphic-git. After that, nothing visible happens until the next login.
The supply-chain angle comes from what gets stolen, which is usually a Git token that can push code. Reading the code, the pollution affects everyone sharing the process, so on a server that handles many users, like a hosted editor or a developer portal, one person’s malicious address could redirect other users’ logins too until the next restart.
What an attacker could do
The confirmed impact is credential theft. The attacker receives whatever usernames, passwords or tokens the app hands out through onAuth, for any Git host the app contacts afterwards in that tab or process, not just for the attacker’s own server. With a token that can push, they can read private repositories, push commits and tamper with code that other people build and install. Since the request passes through their proxy, reading the code they could also forward it on so everything keeps working and nobody notices. Nothing in this bug runs code on the victim’s machine directly.
What it actually takes
The app has to use an isomorphic-git version before 1.42.0, it has to call getRemoteInfo (its newer getRemoteInfo2, clone, fetch and push don’t build this object), and it has to point that call at an address the attacker controls, which usually means a user pasting one in. The theft also needs a later network call in the same tab or process that doesn’t set its own corsProxy, with credentials supplied through onAuth.
The fix
Update to isomorphic-git 1.42.0 or later; the latest as of November 2026 is 1.44.9. The fix in #2426, by jcubic, adds a helper that skips any ref whose path contains __proto__ and only reuses levels of the tree that really belong to it, so names like constructor get a fresh object instead of reaching the template. Tests check that the template stays clean. The CVE record lists all versions before 1.42.0 as affected, which matches the code, while the GitHub advisory names 1.40.0 to 1.41.9. Since npm audit won’t flag it, check the version yourself with npm ls isomorphic-git, including copies pulled in by other packages.
Reporting it
I reported it with VulnCheck, a CNA (one of the organizations allowed to assign CVE numbers), coordinating the disclosure. jcubic opened the fix and merged it about half an hour later, and version 1.42.0, the advisory and CVE-2026-89011 all went public on September 10, 2026. If you maintain something built on isomorphic-git, the upgrade is a one-line change, which is a pleasant price for no longer letting a branch list edit JavaScript itself.
Advisories: CVE-2026-89011 · GHSA-83vg-jxvh-fx76 · VulnCheck · Fix PR #2426