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_execbackend in PR #7163; the equivalent construction inssh_libssh2.cwas 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 tolibssh2_channel_exec()at line 99. OpenSSHsshdhands 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 rawgit_str_put(). A literal'therefore survives;%27does 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()insrc/libgit2/submodule.cbefore being stored.For comparison,
gitapplieslooks_like_command_line_option()and thensq_quote_buf()inconnect.c. The comment on that function in git'spath.hnotes 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=libssh2against libssh2 1.11.1, OpenSSH server on localhost, account with/bin/bashas login shell and its host key already inknown_hosts.
- Create a repository with one submodule added normally, then rewrite only the
urlvalue in.gitmodulesto:ssh://localhost/x';id > /tmp/cve-proof.txt;echo 'The trailing
echo 'exists so the closing quote appended bygen_proto()is balanced; without it the remote shell rejects the whole line.
- From a program using the public API,
git_clone()the repository, thengit_submodule_update()on the submodule, supplying credentials through the standard callback.Result:
/tmp/cve-proof.txton the server contains the output ofidfor the authenticating account. libgit2 reportsfatal: '/x' does not appear to be a git repository, which isgit-upload-packfailing on the first of the three commands after the shell has already split the line.A plain
git_clone()of the same craftedssh://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'sknown_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-shellrejects it insq_dequote(); GitLab and Gitea build argument lists rather than command lines; a forced command puts the string inSSH_ORIGINAL_COMMANDinstead. The affected case is an account with an ordinary login shell.Affected builds are those configured with the libssh2 backend.
cmake/SelectSSH.cmakeselects it for bothUSE_SSH=libssh2andUSE_SSH=ON, and the packaging I checked for Debian, Ubuntu, Alpine, Arch, Homebrew and Fedora passesON.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.combounces withYour 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.mdgivessecurity@libgit2.comwhile libgit2.org givessecurity@libgit2.org.