Skip to content

GHSA-qqwh-747c-fpx2

CVE Information

Summary

gen_proto() in the libssh2 SSH backend builds the remote command string by wrapping the repository path in single quotes without escaping the path. A path containing a single quote closes the quoting early, and the remainder is interpreted by the remote account's login shell.

This is CVE-2026-5917, published by VulnCheck on 2026-08-11. It is still present on main (checked at 95ff8c2, 2026-08-11) and in v1.9.6. Disclosure history is at the end of this report.

The same class was addressed in the ssh_exec backend in PR #7163; the equivalent construction in ssh_libssh2.c was not changed.

Details

src/libgit2/transports/ssh_libssh2.c, gen_proto() lines 79-82:

git_str_puts(request, cmd);
git_str_puts(request, " '");
git_str_puts(request, repo);      /* repo = url->path, not escaped */
git_str_puts(request, "'");

send_command() passes the result to libssh2_channel_exec() at line 99. OpenSSH sshd hands an exec-request string to the account's login shell via -c, so shell metacharacters in it are significant.

The path is not filtered or decoded on the way there:

  • git_net_url_parse() percent-decodes user, password, host, query and fragment, but copies the path with a raw git_str_put(). A literal ' therefore survives; %27 does not become a quote. ? and # terminate the path, so they cannot appear in it. git_net_url_parse_scp() copies the remainder verbatim with no such split.
  • The only check applied is git_process__is_cmdline_option() at line 801 (src/util/process.h:115, return (str && str[0] == '-');), which is a leading-dash test for option injection.
  • Submodule URLs get the same one-character test via looks_like_command_line_option() in src/libgit2/submodule.c before being stored.

For comparison, git applies looks_like_command_line_option() and then sq_quote_buf() in connect.c. The comment on that function in git's path.h notes the dash check has nothing to do with shell quoting and that quoting should be handled separately.

PoC

Environment: Ubuntu 26.04, libgit2 v1.9.0 built with -DUSE_SSH=libssh2 against libssh2 1.11.1, OpenSSH server on localhost, account with /bin/bash as login shell and its host key already in known_hosts.

  1. Create a repository with one submodule added normally, then rewrite only the url value in .gitmodules to:
ssh://localhost/x';id > /tmp/cve-proof.txt;echo '

The trailing echo ' exists so the closing quote appended by gen_proto() is balanced; without it the remote shell rejects the whole line.

  1. From a program using the public API, git_clone() the repository, then git_submodule_update() on the submodule, supplying credentials through the standard callback.

Result: /tmp/cve-proof.txt on the server contains the output of id for the authenticating account. libgit2 reports fatal: '/x' does not appear to be a git repository, which is git-upload-pack failing on the first of the three commands after the shell has already split the line.

A plain git_clone() of the same crafted ssh:// URL reaches the same code with no submodule involved.

Impact

Command execution on the SSH server under the authenticating account, from the client side.

Two preconditions bound it, and I want to state them rather than leave them out:

  • check_certificate() runs in _git_ssh_setup_conn() before any command is sent, so the target host key must already be in the user's known_hosts (or accepted by an application callback). The attacker cannot name an arbitrary host.
  • The receiving account must hand the string to a shell. git-shell rejects it in sq_dequote(); GitLab and Gitea build argument lists rather than command lines; a forced command puts the string in SSH_ORIGINAL_COMMAND instead. The affected case is an account with an ordinary login shell.

Affected builds are those configured with the libssh2 backend. cmake/SelectSSH.cmake selects it for both USE_SSH=libssh2 and USE_SSH=ON, and the packaging I checked for Debian, Ubuntu, Alpine, Arch, Homebrew and Fedora passes ON.

Suggested fix

The helper already exists and is already applied to this same value in the other backend (ssh_exec.c):

git_str_puts(request, cmd);
git_str_puts(request, " '");
git_str_puts_escaped(request, repo, "'!", "'\\", "'");
git_str_puts(request, "'");

Disclosure history

  • 2026-03-07 — Reported to VulnCheck (CNA).
  • 2026-04-09 — VulnCheck allocated CVE-2026-5917 and told me they had initiated outreach to libgit2, with a 120-day deadline of 2026-08-06.
  • 2026-08-11 — VulnCheck published, stating no response was received during that period.
  • 2026-08-13 — This report.

I left vendor contact to the CNA, which is why this is reaching you only now.

I am sending it directly because I do not believe the outreach arrived. Issue #7304 (2026-06-26) reports that security@libgit2.com bounces with Your message to libgit2-security@googlegroups.com has been blocked, from two different sender domains, and it is still open. I cannot confirm whether that was already the case in April, but it seems worth checking independently of this report. SECURITY.md gives security@libgit2.com while libgit2.org gives security@libgit2.org.