Skip to content

Commit a99e622

Browse files
committed
use os.Root to mitigate fs security issues
1 parent e000ae7 commit a99e622

File tree

5 files changed

+69
-19
lines changed

5 files changed

+69
-19
lines changed

cmd/src/servegit.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,18 @@ Documentation at https://sourcegraph.com/docs/admin/code_hosts/src_serve_git
5858
dbug = log.New(os.Stderr, "DBUG serve-git: ", log.LstdFlags)
5959
}
6060

61+
root, err := os.OpenRoot(repoDir)
62+
if err != nil {
63+
return err
64+
}
65+
defer root.Close()
66+
6167
s := &servegit.Serve{
62-
Addr: *addrFlag,
63-
Root: repoDir,
64-
Info: log.New(os.Stderr, "serve-git: ", log.LstdFlags),
65-
Debug: dbug,
68+
Addr: *addrFlag,
69+
Root: repoDir,
70+
RootFS: root,
71+
Info: log.New(os.Stderr, "serve-git: ", log.LstdFlags),
72+
Debug: dbug,
6673
}
6774

6875
if *listFlag {

internal/servegit/gitservice.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"bytes"
66
"compress/gzip"
77
"context"
8+
"io"
89
"net/http"
910
"os"
1011
"os/exec"
@@ -65,6 +66,10 @@ type Handler struct {
6566
// call the returned function when done executing. If the executation
6667
// failed, it will pass in a non-nil error.
6768
Trace func(ctx context.Context, svc, repo, protocol string) func(error)
69+
70+
// RootFS is a traversal safe API that ensures files outside of the
71+
// root cannot be opened.
72+
RootFS *os.Root
6873
}
6974

7075
func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -93,7 +98,10 @@ func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
9398
return
9499
}
95100

96-
if _, err = os.Stat(dir); os.IsNotExist(err) {
101+
// os.Root only accepts relative paths from it's root. So we trim the
102+
// prefix.
103+
relDir := strings.TrimPrefix(dir, s.RootFS.Name()+string(os.PathSeparator))
104+
if _, err = s.RootFS.Stat(relDir); os.IsNotExist(err) {
97105
http.Error(w, "repository not found", http.StatusNotFound)
98106
return
99107
} else if err != nil {
@@ -104,6 +112,7 @@ func (s *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
104112
body := r.Body
105113
defer body.Close()
106114

115+
// TODO(@evict) max filereader
107116
if r.Header.Get("Content-Encoding") == "gzip" {
108117
gzipReader, err := gzip.NewReader(body)
109118
if err != nil {

internal/servegit/gitservice_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"net/http/httptest"
8+
"os"
89
"os/exec"
910
"path/filepath"
1011
"strings"
@@ -31,10 +32,17 @@ func TestHandler(t *testing.T) {
3132
runCmd(t, repo, "git", "tag", fmt.Sprintf("v%d", i+1))
3233
}
3334

35+
rootFS, err := os.OpenRoot(root)
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
t.Cleanup(func() { rootFS.Close() })
40+
3441
ts := httptest.NewServer(&Handler{
3542
Dir: func(_ context.Context, s string) (string, error) {
3643
return filepath.Join(root, s, ".git"), nil
3744
},
45+
RootFS: rootFS,
3846
})
3947
defer ts.Close()
4048

internal/servegit/serve.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"log"
1010
"net"
1111
"net/http"
12+
"os"
1213
"os/exec"
1314
pathpkg "path"
1415
"path/filepath"
@@ -19,10 +20,11 @@ import (
1920
)
2021

2122
type Serve struct {
22-
Addr string
23-
Root string
24-
Info *log.Logger
25-
Debug *log.Logger
23+
Addr string
24+
Root string
25+
RootFS *os.Root
26+
Info *log.Logger
27+
Debug *log.Logger
2628
}
2729

2830
func (s *Serve) Start() error {
@@ -69,7 +71,7 @@ func (s *Serve) handler() http.Handler {
6971

7072
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
7173
w.Header().Set("Content-Type", "text/html; charset=utf-8")
72-
err := indexHTML.Execute(w, map[string]interface{}{
74+
err := indexHTML.Execute(w, map[string]any{
7375
"Explain": explainAddr(s.Addr),
7476
"Links": []string{
7577
"/v1/list-repos",
@@ -100,7 +102,8 @@ func (s *Serve) handler() http.Handler {
100102
_ = enc.Encode(&resp)
101103
})
102104

103-
fs := http.FileServer(http.Dir(s.Root))
105+
safeFS := http.FS(s.RootFS.FS())
106+
fs := http.FileServer(safeFS)
104107
svc := &Handler{
105108
Dir: func(_ context.Context, name string) (string, error) {
106109
return filepath.Join(s.Root, filepath.FromSlash(name)), nil
@@ -117,6 +120,7 @@ func (s *Serve) handler() http.Handler {
117120
}
118121
}
119122
},
123+
RootFS: s.RootFS,
120124
}
121125
mux.Handle("/repos/", http.StripPrefix("/repos/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
122126
// Use git service if git is trying to clone. Otherwise show http.FileServer for convenience

internal/servegit/serve_test.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,21 @@ func TestReposHandler(t *testing.T) {
3636
}}
3737
for _, tc := range cases {
3838
t.Run(tc.name, func(t *testing.T) {
39+
3940
root := gitInitRepos(t, tc.repos...)
4041

42+
rootFS, err := os.OpenRoot(root)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
t.Cleanup(func() { rootFS.Close() })
47+
4148
h := (&Serve{
42-
Info: testLogger(t),
43-
Debug: discardLogger,
44-
Addr: testAddress,
45-
Root: root,
49+
Info: testLogger(t),
50+
Debug: discardLogger,
51+
Addr: testAddress,
52+
Root: root,
53+
RootFS: rootFS,
4654
}).handler()
4755

4856
var want []Repo
@@ -132,6 +140,13 @@ func gitInit(t *testing.T, path string) {
132140

133141
func gitInitRepos(t *testing.T, names ...string) string {
134142
root := t.TempDir()
143+
144+
// We cannot set root on a non-existent dir so we return tmpdir
145+
// creating repos-root first causes issues with test clean-up on Windows.
146+
if len(names) == 0 {
147+
return root
148+
}
149+
135150
root = filepath.Join(root, "repos-root")
136151

137152
for _, name := range names {
@@ -161,10 +176,17 @@ func TestIgnoreGitSubmodules(t *testing.T) {
161176
t.Fatal(err)
162177
}
163178

179+
rootFS, err := os.OpenRoot(root)
180+
if err != nil {
181+
t.Fatal(err)
182+
}
183+
t.Cleanup(func() { rootFS.Close() })
184+
164185
repos, err := (&Serve{
165-
Info: testLogger(t),
166-
Debug: discardLogger,
167-
Root: root,
186+
Info: testLogger(t),
187+
Debug: discardLogger,
188+
Root: root,
189+
RootFS: rootFS,
168190
}).Repos()
169191
if err != nil {
170192
t.Fatal(err)
@@ -201,6 +223,6 @@ type testWriter struct {
201223
}
202224

203225
func (tw testWriter) Write(p []byte) (n int, err error) {
204-
tw.T.Log(string(p))
226+
tw.Log(string(p))
205227
return len(p), nil
206228
}

0 commit comments

Comments
 (0)