package e2e import ( "fmt" "os" "os/exec" "path/filepath" "strings" "testing" ) // sshWrapper is what authorized_keys does, without an sshd: force one command with the account named. func sshWrapper(t *testing.T, in *instance, account string) string { t.Helper() path := filepath.Join(t.TempDir(), "ssh") // git sends its own options and the host before the command, so the command is the last argument. script := fmt.Sprintf("#!/bin/sh\nfor a in \"$@\"; do cmd=\"$a\"; done\n"+ "SSH_ORIGINAL_COMMAND=\"$cmd\" exec %q ssh --config %q --account %q\n", binary, in.cfg.Path, account) if err := os.WriteFile(path, []byte(script), 0o700); err != nil { t.Fatal(err) } return path } // Chapter 10 makes an ssh key the identity, so ssh is the transport, and it has to carry a push. func TestGitOverSSHClonesAndPushes(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") seed(t, in, john, "john", "johnbot") wrapper := sshWrapper(t, in, "john") dir := filepath.Join(t.TempDir(), "clone") sshRun(t, "", wrapper, "git", "clone", "-q", "ssh://barerepo/john/johnbot", dir) if _, err := os.Stat(filepath.Join(dir, "config.go")); err != nil { t.Fatalf("the clone brought nothing: %v", err) } write(t, dir, "config.go", "package main\n\nvar over = \"ssh\"\n") run(t, dir, "git", "-c", "user.email=t@x", "-c", "user.name=t", "commit", "-qam", "pushed over ssh") sshRun(t, dir, wrapper, "git", "push", "-q", "origin", "master") if _, _, page := get(t, in.http.URL+"/john/johnbot"); !strings.Contains(page, "pushed over ssh") { t.Errorf("a push over ssh did not land:\n%s", page) } } // Chapter 41.3: SSH_ORIGINAL_COMMAND is attacker controlled and must never reach a shell. func TestSSHRefusesAnythingThatIsNotGit(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } in := newInstance(t) john := in.account("john") seed(t, in, john, "john", "johnbot") for _, cmd := range []string{ "", "sh", "git-upload-pack 'john/johnbot'; touch /tmp/barerepo-owned", "git-upload-pack 'john/johnbot' && whoami", "scp -t /tmp", // Well formed apart from the verb, so nothing but the list of three can refuse it. "scp 'john/johnbot'", "git-upload-pack '../../etc'", } { out, err := barerepoSSH(t, in, "john", cmd) if err == nil { t.Errorf("%q was accepted over ssh:\n%s", cmd, out) continue } if strings.Contains(out, "panic") || strings.Contains(out, "goroutine ") { t.Errorf("%q crashed rather than being refused:\n%s", cmd, out) } if !strings.Contains(out, "barerepo") { t.Errorf("%q was refused without saying who refused it:\n%s", cmd, out) } } if _, err := os.Stat("/tmp/barerepo-owned"); err == nil { os.Remove("/tmp/barerepo-owned") t.Fatal("a shell ran, so SSH_ORIGINAL_COMMAND reached one") } } // barerepoSSH runs the entry point authorized_keys forces, with one command in the environment. func barerepoSSH(t *testing.T, in *instance, account, cmd string) (string, error) { t.Helper() c := exec.Command(binary, "ssh", "--config", in.cfg.Path, "--account", account) c.Env = append(os.Environ(), "SSH_ORIGINAL_COMMAND="+cmd) out, err := c.CombinedOutput() return string(out), err } // sshRun runs a git command with barerepo's ssh entry point standing in for the daemon. func sshRun(t *testing.T, dir, wrapper, name string, args ...string) { t.Helper() cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+wrapper, "GIT_AUTHOR_NAME=tester", "GIT_AUTHOR_EMAIL=t@x", "GIT_COMMITTER_NAME=tester", "GIT_COMMITTER_EMAIL=t@x", "GIT_TERMINAL_PROMPT=0") if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, out) } }