VYPR
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.

PackageAffected versionsPatched versions
github.com/git-lfs/git-lfsGo
< 2.1.1-0.20170519163204-f913f5f9c7c62.1.1-0.20170519163204-f913f5f9c7c6

Patches

1
f913f5f9c7c6

Merge pull request #2241 from git-lfs/ssh-options-fix

https://github.com/git-lfs/git-lfsTaylor BlauMay 19, 2017via ghsa
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

News mentions

0

No linked articles in our index yet.