Skip to content

Commit 08d669c

Browse files
authored
security: use traversal safe os.Root for FS operations (#1222)
* use os.Root to mitigate fs security issues
1 parent e000ae7 commit 08d669c

File tree

5 files changed

+71
-19
lines changed

5 files changed

+71
-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: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"os"
1010
"os/exec"
11+
"path/filepath"
1112
"strconv"
1213
"strings"
1314

@@ -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,14 @@ 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, err := filepath.Rel(s.RootFS.Name(), dir)
104+
if err != nil {
105+
http.Error(w, "invalid path specified: "+err.Error(), http.StatusBadRequest)
106+
return
107+
}
108+
if _, err = s.RootFS.Stat(relDir); os.IsNotExist(err) {
97109
http.Error(w, "repository not found", http.StatusNotFound)
98110
return
99111
} else 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: 29 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,12 @@ 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 os.OpenRoot on a non-existent dir so we return tmpdir
145+
if len(names) == 0 {
146+
return root
147+
}
148+
135149
root = filepath.Join(root, "repos-root")
136150

137151
for _, name := range names {
@@ -161,10 +175,17 @@ func TestIgnoreGitSubmodules(t *testing.T) {
161175
t.Fatal(err)
162176
}
163177

178+
rootFS, err := os.OpenRoot(root)
179+
if err != nil {
180+
t.Fatal(err)
181+
}
182+
t.Cleanup(func() { rootFS.Close() })
183+
164184
repos, err := (&Serve{
165-
Info: testLogger(t),
166-
Debug: discardLogger,
167-
Root: root,
185+
Info: testLogger(t),
186+
Debug: discardLogger,
187+
Root: root,
188+
RootFS: rootFS,
168189
}).Repos()
169190
if err != nil {
170191
t.Fatal(err)
@@ -201,6 +222,6 @@ type testWriter struct {
201222
}
202223

203224
func (tw testWriter) Write(p []byte) (n int, err error) {
204-
tw.T.Log(string(p))
225+
tw.Log(string(p))
205226
return len(p), nil
206227
}

0 commit comments

Comments
 (0)