CVE-2026-5917 · libgit2 · reported by izzysec

Most developers set up SSH access to a work server once, add their key, and never think about it again, which is the whole appeal. From then on their tools log in for them whenever they need code from that server, and the server trusts them because it trusts the key. The arrangement quietly assumes that your tools only ever ask the server for code, and that nobody else gets to choose what they ask.

In libgit2, a library that a lot of Git-speaking programs are built on, a repository address with a single quote mark in it could break that assumption. When libgit2 used its libssh2 backend to fetch code, it wrapped the address in quotes and sent it to the server without checking whether the address already contained a quote of its own, so a crafted address could end the quoting early and make the server run extra commands, logged in as you. The slightly embarrassing part is that libgit2 has two ways of speaking SSH, and exactly this problem had already been fixed in the other one in December 2025. The libssh2 backend kept it for eight more months, until libgit2 1.9.7 and 1.8.7 in August 2026. The CVE record rates it 8.8 High on CVSS 3.1 and 8.6 High on CVSS 4.0.

The attacker supplies one ssh URL: a host you trust and a path containing a quote, placed in a submodule, a dependency list, a mirror list or an import form. Your tool follows the URL, the SSH login succeeds with a known host key and your own key, libgit2 sends one line, git-upload-pack followed by the quoted path, the server's login shell runs it, the quote ends the path early, and the rest runs as commands on the server as your account.
One address goes in, and the server's shell reads part of it as commands.

What libgit2 actually is

Git, the tool almost every developer uses to track and share code, is really a bundle of command-line programs. That works fine when a person types the commands, but it is awkward when you are building an app, a website or a build system that needs to do Git things itself. libgit2 is a rewrite of Git’s core as a library that other programs can include directly. Shawn O. Pearce wrote the first draft in October 2008, the project moved to GitHub in 2010, and it is licensed under GPLv2 with a linking exception, so even closed-source products can use it. The libgit2 website lists GitHub, GitLab, Bitbucket, Microsoft and Canonical among its users, and the main repository has about 10.6k stars as of November 2026.

Most people never touch libgit2 directly. They use it through a binding, a wrapper that makes it usable from another programming language:

Binding Language Downloads
git2 Rust 124M total
Rugged Ruby 91M total
LibGit2Sharp .NET 34M total
pygit2 Python 4.6M last month
nodegit Node.js 126K last month

Numbers as of November 2026. Not all of these are affected, because it depends on how libgit2 was built, which is covered below.

How the attack works

A few terms, once:

  • SSH: the standard way to log in to a remote server securely. Git uses it to fetch and push code, usually logging in with a key file instead of a password.
  • libssh2 backend and OpenSSH backend: libgit2 can speak SSH in two ways, either through libssh2, an SSH library compiled into the program, or by running the computer’s own ssh command. Whoever builds libgit2 picks one, and the default is no SSH at all.
  • Login shell: the command interpreter, such as bash, that a server account uses. When a program sends a command over SSH, the server hands the text to this shell to run.
  • git-shell: a restricted shell that only accepts Git’s own commands, often used for accounts that exist just to host repositories.
  • Submodule: a project embedded inside another project by its address, which tools fetch separately.

When libgit2 fetches code over SSH, it logs in and asks the server to run one command, git-upload-pack, followed by the repository’s path wrapped in single quotes. Single quotes tell the server’s shell to treat everything between them as one piece of text, which is a perfectly good plan as long as the text does not contain a single quote itself. libgit2’s libssh2 backend copied the path from the address as it was, so a quote in the path closed the quoting early, and the shell read everything after it as more commands. The only check on the path was that it should not start with a dash, which stops a different trick and does nothing about this one. Going by the source of older releases, the command has been put together this way since libgit2 first got SSH support in 2013.

libgit2 doesn’t update submodules on its own, but the programs built on it often do, because build tools need the submodule’s code to compile anything. libgit2 also follows include lines in a project’s .gitmodules file, something Git itself ignores there, so the crafted address does not even have to appear in the file a reviewer would actually read. The full technical write-up, including how I reproduced it, is in the advisory.

Which builds use the libssh2 backend matters a lot here. The Rust git2 crate uses it when its ssh feature is turned on, pygit2’s Linux and macOS packages ship with it, nodegit compiles it in, and so do Homebrew’s libgit2 and Julia. LibGit2Sharp’s current build scripts use the OpenSSH backend instead, which already had the fix, and Rugged only includes SSH if you ask for it when installing.

Attacker path, start to finish:

  1. The attacker picks a server that the target already logs into over SSH with an ordinary account, such as a company’s self-hosted Git box or build server. They don’t need any access to it themselves.
  2. They write an SSH address that points at that real server, with a repository path that contains a quote followed by the commands they want to run there.
  3. They put that address somewhere the target’s tool will follow it, most conveniently as a submodule in a project the target builds, tucked into an included file, or else in a list of Git dependencies, a mirror list, or the “import a repository” field of a service built on libgit2.
  4. The target does something completely normal, like building the project, and the tool fetches the submodule. The server’s identity checks out because it really is the trusted server, and the target’s own key logs in.
  5. The server’s login shell runs the extra commands as the target’s account, and the fetch then fails with an error that looks like any other broken submodule.

Fixed once, missed once

None of this was new to libgit2, since the OpenSSH backend had the same unescaped path and was fixed in libgit2 1.9.2 and 1.8.5 on December 6, 2025, in a commit titled “ssh_exec: escape remote paths properly” (#7163). The libssh2 backend builds the same command from the same path in a different file, and nobody carried the fix across, so for eight months libgit2 had one SSH backend that escaped quotes and one that did not. The 1.9.7 release notes describe the eventual fix rather honestly as bringing “the same escaping to the libssh2 transport”.

What an attacker could do

The commands run on the SSH server named in the address, as the account the victim’s key logs in to, and not on the victim’s own computer. From there an attacker could read or change the repositories that account owns, or add their own key to its list of authorized keys and come back whenever they like without needing the victim again. On a self-hosted Git server that is a supply-chain foothold, since quietly editing a repository there reaches everyone who builds from it. If the victim is a service that imports repositories from addresses its users type in, the account in question is the service’s own, on whatever server that service can log in to.

What it actually takes

It doesn’t work against any server you like. The victim’s program has to use libgit2 built with the libssh2 backend, and it has to actually connect to the crafted address, which means someone has to build the project, update its submodules or import the URL. The server must be one the victim’s program already trusts and can log in to with the victim’s key. The account on that server must also have an ordinary login shell. Accounts locked to git-shell are not affected, because git-shell refuses an argument with broken quoting, and accounts with a forced command never pass the text to a shell at all, which I checked by running the same address against a shell account and a forced-command account. Dedicated Git hosting is normally set up one of those ways, so the realistic targets are ordinary user accounts on internal servers. CISA’s assessment in the CVE record lists no known exploitation.

The fix

Update to libgit2 1.9.7 or 1.8.7, both released on August 13, 2026. The fix in #7347, by Stephen Gallagher, is a one-line change that runs the path through the same escaping function the OpenSSH backend already used, so a quote in the path stays part of the path. Everything before 1.8.7, and 1.9.0 through 1.9.6, is affected.

Most people get libgit2 through something else, so the version that matters is the one bundled in your tool. pygit2’s packages moved to libgit2 1.9.7 in pygit2 1.20.1 on September 12, 2026, the Rust libgit2-sys crate shipped it as 0.18.8+1.9.7 on August 21, 2026, Homebrew’s libgit2 is on 1.9.7, and Debian fixed its packages in DSA-6453-1. nodegit and Rugged had not shipped releases with the fixed libgit2 as of November 2026. On the server side, putting accounts that only exist to host repositories behind git-shell blocks this whole class of trick regardless of which client connects.

If you build anything on libgit2 with SSH, it is worth checking which backend you compiled in and which libgit2 version is actually bundled, because the answer to both is easy to forget.

Two reports, one CVE

I found this bug in August 2026 and reported it straight to libgit2’s maintainer, who thanked me and went off to request a CVE for it, only to find that one had just been published. Kazuma Matsumoto had found the same bug months earlier and reported it in March to VulnCheck, which is a CNA, one of the organizations allowed to hand out CVE numbers. VulnCheck gives vendors 120 days by default, and when that deadline passed without a fix it published CVE-2026-5917 on August 11. So I had the slightly surreal experience of independently finding a bug at about the moment the rest of the world was being told about it.

My report is the one that turned into the fix, as Stephen Gallagher opened pull request #7347 on August 13, it was merged and released that same day, and the maintainer thanked him for “effectively mitigating a zero day (or in fact a -2 day)”. The CVE record credits “Kazuma Matsumoto, a security researcher at GMO Cybersecurity by IERAE, Inc.” and VulnCheck was kind enough to add me to the credits as well, which is a far nicer ending than two researchers arguing over who owns a quote mark.

Advisories: CVE-2026-5917 · GHSA-xqj4-2j5v-rr75 · VulnCheck · Fix PR #7347