package e2e import ( "io" "net/http" "os/exec" "strings" "testing" ) // Chapter 45.4's attacks, kept permanently, and these need a real push or request. func TestSecurityList(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") t.Run("a branch that is really an argument", func(t *testing.T) { c := clone(t, in, john, "/john/johnbot") for _, ref := range []string{ "refs/heads/--upload-pack=/bin/sh", "refs/heads/-x", "refs/heads/..", } { out, err := try(t, c, "git", "push", in.url(john, "/john/johnbot"), "HEAD:"+ref) if err == nil { t.Errorf("%s was accepted:\n%s", ref, out) } } }) t.Run("a repository named ../../etc", func(t *testing.T) { c := clone(t, in, john, "/john/johnbot") for _, path := range []string{"/john/..%2f..%2fetc", "/john/../../etc", "/../../etc/passwd"} { out, err := try(t, c, "git", "push", in.url(john, path), "master") if err == nil { t.Errorf("a push to %s was accepted:\n%s", path, out) } } }) t.Run("evil.html fetched raw", func(t *testing.T) { // A file with a script in it, served from this origin, is the reader's session. 42.3. c := clone(t, in, john, "/john/johnbot") write(t, c, "evil.html", "\n") run(t, c, "git", "add", "-A") run(t, c, "git", "commit", "-qm", "add evil.html") run(t, c, "git", "push", "-q", in.url(john, "/john/johnbot"), "master") resp, err := http.Get(in.http.URL + "/john/johnbot/raw/master/evil.html") if err != nil { t.Fatal(err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if !strings.Contains(string(body), "alert(document.cookie)") { t.Fatalf("the file did not come back at all: %s", body) } want := map[string]string{ "Content-Type": "text/plain; charset=utf-8", "Content-Disposition": "attachment", "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "default-src 'none'; sandbox", } for header, value := range want { if got := resp.Header.Get(header); got != value { t.Errorf("%s = %q, want %q: the browser would run this file", header, got, value) } } }) t.Run("a private repository over git", func(t *testing.T) { // No .barerepo/config in the content, or the file decides and the default never applies. c := t.TempDir() run(t, c, "git", "init", "-q", "-b", "master") write(t, c, "secret.txt", "do not read\n") run(t, c, "git", "add", "-A") run(t, c, "git", "commit", "-qm", "private things") run(t, c, "git", "push", "-q", in.url(john, "/john/hidden"), "master") lisa := in.account("lisa") if out, err := try(t, "", "git", "ls-remote", in.url(lisa, "/john/hidden")); err == nil { t.Errorf("another account listed a private repository:\n%s", out) } if out, err := try(t, "", "git", "ls-remote", in.http.URL+"/john/hidden"); err == nil { t.Errorf("an anonymous client listed a private repository:\n%s", out) } }) t.Run("a config that says public makes it public", func(t *testing.T) { // A pushed visibility = "public" is chapter 14 working, not the default failing. c := clone(t, in, john, "/john/johnbot") run(t, c, "git", "push", "-q", in.url(john, "/john/open"), "master") if _, err := try(t, "", "git", "ls-remote", in.http.URL+"/john/open"); err != nil { t.Error("a repository whose config says public was not readable") } }) t.Run("a token is not a session", func(t *testing.T) { // A git token must not open the interface, or one in a build script is a login. req, _ := http.NewRequest(http.MethodGet, in.http.URL+"/keys", nil) req.AddCookie(&http.Cookie{Name: "barerepo_session", Value: john}) resp, err := http.DefaultTransport.RoundTrip(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { t.Error("a git token worked as a session cookie") } }) }