High severity8.8NVD Advisory· Published Dec 21, 2017· Updated May 13, 2026
CVE-2017-17831
CVE-2017-17831
Description
GitHub Git LFS before 2.1.1 allows remote attackers to execute arbitrary commands via an ssh URL with an initial dash character in the hostname, located on a "url =" line in a .lfsconfig file within a repository.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/git-lfs/git-lfsGo | < 2.1.1-0.20170519163204-f913f5f9c7c6 | 2.1.1-0.20170519163204-f913f5f9c7c6 |
Patches
1f913f5f9c7c6Merge pull request #2241 from git-lfs/ssh-options-fix
7 files changed · +221 −18
lfsapi/ssh.go+36 −11 modified@@ -6,6 +6,7 @@ import ( "fmt" "os/exec" "path/filepath" + "regexp" "strings" "time" @@ -105,13 +106,11 @@ func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, err } func sshGetLFSExeAndArgs(osEnv Env, e Endpoint, method string) (string, []string) { - operation := endpointOperation(e, method) - tracerx.Printf("ssh: %s git-lfs-authenticate %s %s", - e.SshUserAndHost, e.SshPath, operation) - exe, args := sshGetExeAndArgs(osEnv, e) - return exe, append(args, - fmt.Sprintf("git-lfs-authenticate %s %s", e.SshPath, operation)) + operation := endpointOperation(e, method) + args = append(args, fmt.Sprintf("git-lfs-authenticate %s %s", e.SshPath, operation)) + tracerx.Printf("run_command: %s %s", exe, strings.Join(args, " ")) + return exe, args } // Return the executable name for ssh on this machine and the base args @@ -129,9 +128,12 @@ func sshGetExeAndArgs(osEnv Env, e Endpoint) (exe string, baseargs []string) { } if ssh == "" { - ssh = "ssh" - } else { - basessh := filepath.Base(ssh) + ssh = defaultSSHCmd + } + + basessh := filepath.Base(ssh) + + if basessh != defaultSSHCmd { // Strip extension for easier comparison if ext := filepath.Ext(basessh); len(ext) > 0 { basessh = basessh[:len(basessh)-len(ext)] @@ -140,7 +142,7 @@ func sshGetExeAndArgs(osEnv Env, e Endpoint) (exe string, baseargs []string) { isTortoise = strings.EqualFold(basessh, "tortoiseplink") } - args := make([]string, 0, 4+len(cmdArgs)) + args := make([]string, 0, 5+len(cmdArgs)) if len(cmdArgs) > 0 { args = append(args, cmdArgs...) } @@ -158,7 +160,30 @@ func sshGetExeAndArgs(osEnv Env, e Endpoint) (exe string, baseargs []string) { } args = append(args, e.SshPort) } - args = append(args, e.SshUserAndHost) + + if sep, ok := sshSeparators[basessh]; ok { + // inserts a separator between cli -options and host/cmd commands + // example: $ ssh -p 12345 -- user@host.com git-lfs-authenticate ... + args = append(args, sep, e.SshUserAndHost) + } else { + // no prefix supported, strip leading - off host to prevent cmd like: + // $ git config lfs.url ssh://-proxycmd=whatever + // $ plink -P 12345 -proxycmd=foo git-lfs-authenticate ... + // + // Instead, it'll attempt this, and eventually return an error + // $ plink -P 12345 proxycmd=foo git-lfs-authenticate ... + args = append(args, sshOptPrefixRE.ReplaceAllString(e.SshUserAndHost, "")) + } return ssh, args } + +const defaultSSHCmd = "ssh" + +var ( + sshOptPrefixRE = regexp.MustCompile(`\A\-+`) + sshSeparators = map[string]string{ + "ssh": "--", + "lfs-ssh-echo": "--", // used in lfs integration tests only + } +)
lfsapi/ssh_test.go+123 −2 modified@@ -2,6 +2,7 @@ package lfsapi import ( "errors" + "net/url" "path/filepath" "testing" "time" @@ -222,13 +223,15 @@ func TestSSHGetLFSExeAndArgs(t *testing.T) { exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), endpoint, "GET") assert.Equal(t, "ssh", exe) assert.Equal(t, []string{ + "--", "user@foo.com", "git-lfs-authenticate user/repo download", }, args) exe, args = sshGetLFSExeAndArgs(cli.OSEnv(), endpoint, "HEAD") assert.Equal(t, "ssh", exe) assert.Equal(t, []string{ + "--", "user@foo.com", "git-lfs-authenticate user/repo download", }, args) @@ -237,6 +240,7 @@ func TestSSHGetLFSExeAndArgs(t *testing.T) { exe, args = sshGetLFSExeAndArgs(cli.OSEnv(), endpoint, "POST") assert.Equal(t, "ssh", exe) assert.Equal(t, []string{ + "--", "user@foo.com", "git-lfs-authenticate user/repo download", }, args) @@ -245,6 +249,7 @@ func TestSSHGetLFSExeAndArgs(t *testing.T) { exe, args = sshGetLFSExeAndArgs(cli.OSEnv(), endpoint, "POST") assert.Equal(t, "ssh", exe) assert.Equal(t, []string{ + "--", "user@foo.com", "git-lfs-authenticate user/repo upload", }, args) @@ -262,7 +267,7 @@ func TestSSHGetExeAndArgsSsh(t *testing.T) { exe, args := sshGetExeAndArgs(cli.OSEnv(), endpoint) assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"user@foo.com"}, args) + assert.Equal(t, []string{"--", "user@foo.com"}, args) } func TestSSHGetExeAndArgsSshCustomPort(t *testing.T) { @@ -278,7 +283,7 @@ func TestSSHGetExeAndArgsSshCustomPort(t *testing.T) { exe, args := sshGetExeAndArgs(cli.OSEnv(), endpoint) assert.Equal(t, "ssh", exe) - assert.Equal(t, []string{"-p", "8888", "user@foo.com"}, args) + assert.Equal(t, []string{"-p", "8888", "--", "user@foo.com"}, args) } func TestSSHGetExeAndArgsPlink(t *testing.T) { @@ -409,6 +414,122 @@ func TestSSHGetExeAndArgsSshCommandCustomPort(t *testing.T) { assert.Equal(t, []string{"-p", "8888", "user@foo.com"}, args) } +func TestSSHGetLFSExeAndArgsWithCustomSSH(t *testing.T) { + cli, err := NewClient(UniqTestEnv(map[string]string{ + "GIT_SSH": "not-ssh", + }), nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://git@host.com:12345/repo") + require.Nil(t, err) + + e := endpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "12345", e.SshPort) + assert.Equal(t, "git@host.com", e.SshUserAndHost) + assert.Equal(t, "repo", e.SshPath) + + exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), e, "GET") + assert.Equal(t, "not-ssh", exe) + assert.Equal(t, []string{"-p", "12345", "git@host.com", "git-lfs-authenticate repo download"}, args) +} + +func TestSSHGetLFSExeAndArgsInvalidOptionsAsHost(t *testing.T) { + cli, err := NewClient(nil, nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://-oProxyCommand=gnome-calculator/repo") + require.Nil(t, err) + assert.Equal(t, "-oProxyCommand=gnome-calculator", u.Host) + + e := endpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SshUserAndHost) + assert.Equal(t, "repo", e.SshPath) + + exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), e, "GET") + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "-oProxyCommand=gnome-calculator", "git-lfs-authenticate repo download"}, args) +} + +func TestSSHGetLFSExeAndArgsInvalidOptionsAsHostWithCustomSSH(t *testing.T) { + cli, err := NewClient(UniqTestEnv(map[string]string{ + "GIT_SSH": "not-ssh", + }), nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://--oProxyCommand=gnome-calculator/repo") + require.Nil(t, err) + assert.Equal(t, "--oProxyCommand=gnome-calculator", u.Host) + + e := endpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "--oProxyCommand=gnome-calculator", e.SshUserAndHost) + assert.Equal(t, "repo", e.SshPath) + + exe, args := sshGetLFSExeAndArgs(cli.OSEnv(), e, "GET") + assert.Equal(t, "not-ssh", exe) + assert.Equal(t, []string{"oProxyCommand=gnome-calculator", "git-lfs-authenticate repo download"}, args) +} + +func TestSSHGetExeAndArgsInvalidOptionsAsHost(t *testing.T) { + cli, err := NewClient(nil, nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://-oProxyCommand=gnome-calculator") + require.Nil(t, err) + assert.Equal(t, "-oProxyCommand=gnome-calculator", u.Host) + + e := endpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SshUserAndHost) + assert.Equal(t, "", e.SshPath) + + exe, args := sshGetExeAndArgs(cli.OSEnv(), e) + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "-oProxyCommand=gnome-calculator"}, args) +} + +func TestSSHGetExeAndArgsInvalidOptionsAsPath(t *testing.T) { + cli, err := NewClient(nil, nil) + require.Nil(t, err) + + u, err := url.Parse("ssh://git@git-host.com/-oProxyCommand=gnome-calculator") + require.Nil(t, err) + assert.Equal(t, "git-host.com", u.Host) + + e := endpointFromSshUrl(u) + t.Logf("ENDPOINT: %+v", e) + assert.Equal(t, "git@git-host.com", e.SshUserAndHost) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SshPath) + + exe, args := sshGetExeAndArgs(cli.OSEnv(), e) + assert.Equal(t, "ssh", exe) + assert.Equal(t, []string{"--", "git@git-host.com"}, args) +} + +func TestParseBareSSHUrl(t *testing.T) { + e := endpointFromBareSshUrl("git@git-host.com:repo.git") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "git@git-host.com", e.SshUserAndHost) + assert.Equal(t, "repo.git", e.SshPath) + + e = endpointFromBareSshUrl("git@git-host.com/should-be-a-colon.git") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "", e.SshUserAndHost) + assert.Equal(t, "", e.SshPath) + + e = endpointFromBareSshUrl("-oProxyCommand=gnome-calculator") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "", e.SshUserAndHost) + assert.Equal(t, "", e.SshPath) + + e = endpointFromBareSshUrl("git@git-host.com:-oProxyCommand=gnome-calculator") + t.Logf("endpoint: %+v", e) + assert.Equal(t, "git@git-host.com", e.SshUserAndHost) + assert.Equal(t, "-oProxyCommand=gnome-calculator", e.SshPath) +} + func TestSSHGetExeAndArgsPlinkCommand(t *testing.T) { plink := filepath.Join("Users", "joebloggs", "bin", "plink.exe")
test/cmd/lfs-ssh-echo.go+18 −3 renamed@@ -19,14 +19,29 @@ type sshResponse struct { func main() { // expect args: - // ssh-echo -p PORT git@127.0.0.1 git-lfs-authenticate REPO OPERATION - if len(os.Args) != 5 { + // lfs-ssh-echo -p PORT -- git@127.0.0.1 git-lfs-authenticate REPO OPERATION + if len(os.Args) != 6 { fmt.Fprintf(os.Stderr, "got %d args: %v", len(os.Args), os.Args) os.Exit(1) } + if os.Args[1] != "-p" { + fmt.Fprintf(os.Stderr, "$1 expected \"-p\", got %q", os.Args[1]) + os.Exit(1) + } + + if os.Args[3] != "--" { + fmt.Fprintf(os.Stderr, "$3 expected \"--\", got %q", os.Args[3]) + os.Exit(1) + } + + if os.Args[4] != "git@127.0.0.1" { + fmt.Fprintf(os.Stderr, "$4 expected \"git@127.0.0.1\", got %q", os.Args[4]) + os.Exit(1) + } + // just "git-lfs-authenticate REPO OPERATION" - authLine := strings.Split(os.Args[4], " ") + authLine := strings.Split(os.Args[5], " ") if len(authLine) < 13 { fmt.Fprintf(os.Stderr, "bad git-lfs-authenticate line: %s\nargs: %v", authLine, os.Args) }
test/cmd/lfs-ssh-proxy-test.go+9 −0 added@@ -0,0 +1,9 @@ +// +build testtools + +package main + +import "fmt" + +func main() { + fmt.Println("SSH PROXY TEST called") +}
test/test-env.sh+1 −1 modified@@ -673,7 +673,7 @@ begin_test "env with multiple ssh remotes" SSH=git@git-server.com:user/repo.git Endpoint (other)=https://other-git-server.com/user/repo.git/info/lfs (auth=none) SSH=git@other-git-server.com:user/repo.git -GIT_SSH=ssh-echo' +GIT_SSH=lfs-ssh-echo' contains_same_elements "$expected" "$(git lfs env | grep -e "Endpoint" -e "SSH=")" )
test/testenv.sh+1 −1 modified@@ -120,7 +120,7 @@ TESTHOME="$REMOTEDIR/home" GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0 -GIT_SSH=ssh-echo +GIT_SSH=lfs-ssh-echo APPVEYOR_REPO_COMMIT_MESSAGE="test: env test should look for GIT_SSH too" export CREDSDIR
test/test-ssh.sh+33 −0 added@@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +. "test/testlib.sh" + +begin_test "ssh with proxy command in lfs.url" +( + set -e + + reponame="batch-ssh-proxy" + setup_remote_repo "$reponame" + clone_repo "$reponame" "$reponame" + + sshurl="${GITSERVER/http:\/\//ssh://-oProxyCommand=ssh-proxy-test/}/$reponame" + echo $sshurl + git config lfs.url "$sshurl" + + contents="test" + oid="$(calc_oid "$contents")" + git lfs track "*.dat" + printf "$contents" > test.dat + git add .gitattributes test.dat + git commit -m "initial commit" + + git push origin master 2>&1 | tee push.log + if [ "0" -eq "${PIPESTATUS[0]}" ]; then + echo >&2 "fatal: push succeeded" + exit 1 + fi + + grep "got 4 args" push.log + grep "lfs-ssh-echo -- -oProxyCommand" push.log +) +end_test
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
11- github.com/git-lfs/git-lfs/pull/2242nvdPatchThird Party AdvisoryWEB
- blog.recurity-labs.com/2017-08-10/scm-vulnsnvdExploitThird Party AdvisoryWEB
- www.securityfocus.com/bid/102926nvdThird Party AdvisoryVDB EntryWEB
- confluence.atlassian.com/sourcetreekb/sourcetree-security-advisory-2018-01-24-942834324.htmlnvdThird Party AdvisoryWEB
- github.com/advisories/GHSA-w4xh-w33p-4v29ghsaADVISORY
- github.com/git-lfs/git-lfs/releases/tag/v2.1.1nvdRelease NotesThird Party AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2017-17831ghsaADVISORY
- github.com/git-lfs/git-lfs/commit/f913f5f9c7c6d1301785fdf9884a2942d59cdf19ghsaWEB
- github.com/git-lfs/git-lfs/pull/2241ghsaWEB
- pkg.go.dev/vuln/GO-2021-0073ghsaWEB
- web.archive.org/web/20200227131639/http://www.securityfocus.com/bid/102926ghsaWEB
News mentions
0No linked articles in our index yet.