From b920b8bf19ea1b0d4676610a7a84dc728c340101 Mon Sep 17 00:00:00 2001 From: Carter Brainerd Date: Tue, 7 Jul 2026 10:01:01 -0400 Subject: [PATCH] fix/security: do not serve arbitrary files over network with serve-git --- internal/servegit/serve.go | 9 +++++---- internal/servegit/serve_test.go | 17 +++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/servegit/serve.go b/internal/servegit/serve.go index 33e1a0e105..99c4f84405 100644 --- a/internal/servegit/serve.go +++ b/internal/servegit/serve.go @@ -102,8 +102,6 @@ func (s *Serve) handler() http.Handler { _ = enc.Encode(&resp) }) - safeFS := http.FS(s.RootFS.FS()) - fs := http.FileServer(safeFS) svc := &Handler{ Dir: func(_ context.Context, name string) (string, error) { return filepath.Join(s.Root, filepath.FromSlash(name)), nil @@ -123,14 +121,17 @@ func (s *Serve) handler() http.Handler { RootFS: s.RootFS, } mux.Handle("/repos/", http.StripPrefix("/repos/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Use git service if git is trying to clone. Otherwise show http.FileServer for convenience + // Only serve the smart Git HTTP endpoints used for cloning/fetching. + // We deliberately do not fall back to a file server: doing so would + // expose arbitrary files under the served root to unauthenticated + // clients (see VULN-99 / HackerOne #3814799). for _, suffix := range []string{"/info/refs", "/git-upload-pack"} { if strings.HasSuffix(r.URL.Path, suffix) { svc.ServeHTTP(w, r) return } } - fs.ServeHTTP(w, r) + http.NotFound(w, r) }))) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/servegit/serve_test.go b/internal/servegit/serve_test.go index a23523fcc4..8ae6bc6644 100644 --- a/internal/servegit/serve_test.go +++ b/internal/servegit/serve_test.go @@ -97,14 +97,15 @@ func testReposHandler(t *testing.T, h http.Handler, repos []Repo) { } } - // repos page will list the top-level dirs - list := get("/repos/") - for _, repo := range repos { - if path.Dir(repo.URI) != "/repos" { - continue - } - if !strings.Contains(repo.Name, "/") && !strings.Contains(list, repo.Name) { - t.Errorf("repos page does not contain substring %q", repo.Name) + // Directory browsing under /repos/ is intentionally disabled to avoid + // exposing arbitrary files (VULN-99). Only smart Git HTTP endpoints are + // served; anything else returns 404. + if res, err := http.Get(ts.URL + "/repos/"); err != nil { + t.Fatal(err) + } else { + res.Body.Close() + if res.StatusCode != http.StatusNotFound { + t.Errorf("GET /repos/ status = %d, want %d", res.StatusCode, http.StatusNotFound) } }