Skip to content

Commit 4544463

Browse files
committed
sender: correctly use os.Root for localDir vs. requested path
Previously, we concatenated the attacker-supplied requested path directly to the localDir, which is not safe. Instead, now we use os.Root to open localDir (configured), then pass the attacker-supplied requested path to that safe API. related to #48
1 parent 2828e20 commit 4544463

File tree

3 files changed

+106
-111
lines changed

3 files changed

+106
-111
lines changed

integration/sender/sender_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,44 @@ func TestSenderRelative(t *testing.T) {
363363
}
364364
}
365365

366+
func TestSenderTraversal(t *testing.T) {
367+
tmp := t.TempDir()
368+
source := filepath.Join(tmp, "source")
369+
dest := filepath.Join(tmp, "dest")
370+
371+
if err := os.MkdirAll(source, 0755); err != nil {
372+
t.Fatal(err)
373+
}
374+
if err := os.WriteFile(filepath.Join(source, "hello.txt"), []byte("hi"), 0644); err != nil {
375+
t.Fatal(err)
376+
}
377+
if err := os.WriteFile(filepath.Join(tmp, "passwd"), []byte("secret"), 0644); err != nil {
378+
t.Fatal(err)
379+
}
380+
381+
if err := os.MkdirAll(dest, 0755); err != nil {
382+
t.Fatal(err)
383+
}
384+
385+
// start a server to sync from
386+
srv := rsynctest.New(t, rsynctest.InteropModule(source))
387+
388+
args := []string{
389+
"gokr-rsync",
390+
"-aH",
391+
"rsync://localhost:" + srv.Port + "/interop/../",
392+
dest + "/",
393+
}
394+
rsynctest.Run(t, args...)
395+
396+
passwd := filepath.Join(dest, "passwd")
397+
398+
got, err := os.ReadFile(passwd)
399+
if err == nil {
400+
t.Fatalf("unexpectedly synced /etc/passwd: %s", string(got))
401+
}
402+
}
403+
366404
// like TestSender, but both source and dest are local directories
367405
func TestSenderBothLocal(t *testing.T) {
368406
t.Parallel()

internal/sender/flist.go

Lines changed: 57 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package sender
22

33
import (
4-
"fmt"
54
"io/fs"
65
"os"
76
"os/user"
87
"path/filepath"
9-
"slices"
108
"strconv"
119
"strings"
1210
"sync"
@@ -58,82 +56,50 @@ var (
5856
lookupGroupOnce sync.Once
5957
)
6058

61-
func getRootStrip(requested, localDir string) (string, string) {
62-
root := filepath.Clean(filepath.Join(localDir, requested))
63-
59+
func getStrip(requested string) string {
6460
sep := string(os.PathSeparator)
65-
strip := filepath.Dir(filepath.Clean(root)) + sep
61+
if requested == sep {
62+
return ""
63+
}
6664
if strings.HasSuffix(requested, sep) {
67-
strip = filepath.Clean(root) + sep
65+
return strings.TrimPrefix(filepath.Clean(requested), "/") + sep
6866
}
69-
return root, strip
67+
return ""
7068
}
7169

7270
type scopedWalker struct {
73-
st *Transfer
74-
ioError func(err error)
75-
conn *rsyncwire.Conn
76-
fec *rsyncwire.Buffer
77-
excl *filterRuleList
78-
uidMap map[int32]string
79-
gidMap map[int32]string
80-
fileList *fileList
81-
prefix string
82-
rootPath string
83-
root *os.Root
71+
st *Transfer
72+
ioError func(err error)
73+
conn *rsyncwire.Conn
74+
fec *rsyncwire.Buffer
75+
excl *filterRuleList
76+
uidMap map[int32]string
77+
gidMap map[int32]string
78+
fileList *fileList
79+
root *os.Root
80+
localDir string
81+
requested string
82+
strip string
8483
}
8584

8685
func (s *scopedWalker) walk() error {
87-
root, err := os.OpenRoot(s.rootPath)
86+
root, err := os.OpenRoot(s.localDir)
8887
if err != nil {
89-
if st, err := os.Stat(s.rootPath); err == nil && !st.IsDir() {
90-
// Not a directory? Open the parent directory, stat the file and
91-
// call the WalkFn directly for this entry.
92-
return s.walkFile()
93-
}
88+
s.st.Logger.Printf(" OpenRoot(localDir=%q): %v", s.localDir, err)
9489
// File does not exist or we cannot open the file?
9590
// Set the I/O error flag, but keep going.
9691
s.ioError(err)
9792
return nil
9893
}
9994
s.fileList.Roots = append(s.fileList.Roots, root)
10095
s.root = root
101-
if err := fs.WalkDir(root.FS(), ".", s.walkFn); err != nil {
102-
return err
103-
}
104-
return nil
105-
}
106-
107-
func (s *scopedWalker) walkFile() error {
108-
dir, file := filepath.Split(s.rootPath)
109-
if s.st.Opts.DebugGTE(rsyncopts.DEBUG_FLIST, 1) {
110-
s.st.Logger.Printf("treating rootPath=%q as dir=%q + file=%q", s.rootPath, dir, file)
111-
}
112-
s.prefix = ""
113-
root, err := os.OpenRoot(dir)
114-
if err != nil {
115-
// set the I/O error flag, but keep going
116-
s.ioError(err)
117-
return nil
118-
}
119-
f, err := root.Open(".")
120-
if err != nil {
121-
return err
122-
}
123-
defer f.Close()
124-
dirents, err := f.ReadDir(-1)
125-
if err != nil {
126-
return err
96+
rootname := s.requested
97+
// fs.WalkDir(root.FS(), …) does not accept absolute paths,
98+
// so make them relative by prepending a .
99+
if strings.HasPrefix(rootname, "/") {
100+
rootname = "." + rootname
127101
}
128-
idx := slices.IndexFunc(dirents, func(dirent os.DirEntry) bool {
129-
return dirent.Name() == file
130-
})
131-
if idx == -1 {
132-
return fmt.Errorf("file %s not found in %s", file, dir)
133-
}
134-
s.fileList.Roots = append(s.fileList.Roots, root)
135-
s.root = root
136-
if err := s.walkFn(file, dirents[idx], nil); err != nil {
102+
if err := fs.WalkDir(root.FS(), filepath.Clean(rootname), s.walkFn); err != nil {
137103
return err
138104
}
139105
return nil
@@ -166,15 +132,14 @@ func (s *scopedWalker) walkFn(path string, d fs.DirEntry, err error) error {
166132
// Only ever transmit long names, like openrsync
167133
flags := byte(rsync.XMIT_LONG_NAME)
168134

169-
name := s.prefix + path
135+
name := path
136+
if s.strip != "" {
137+
name = strings.TrimPrefix(name, s.strip)
138+
}
170139
if opts.DebugGTE(rsyncopts.DEBUG_FLIST, 1) {
171140
logger.Printf("Trim(path=%q) = %q", path, name)
172141
}
173142
if path == "." {
174-
name = s.prefix
175-
if s.prefix == "" {
176-
name = "."
177-
}
178143
flags |= rsync.XMIT_TOP_DIR
179144
}
180145
// st.logger.Printf("flags for %q: %v", name, flags)
@@ -372,33 +337,41 @@ func (st *Transfer) SendFileList(localDir string, paths []string, excl *filterRu
372337
}
373338

374339
for _, requested := range paths {
340+
local := localDir
341+
if local == "/" {
342+
// Implicit module (/) and absolute requested path (/tmp/foo/),
343+
// turn the path into the local directory and request /.
344+
local = requested
345+
if strings.HasSuffix(requested, string(os.PathSeparator)) {
346+
requested = "/"
347+
} else {
348+
local = filepath.Dir(requested)
349+
requested = filepath.Base(requested)
350+
}
351+
}
352+
375353
if st.Opts.DebugGTE(rsyncopts.DEBUG_FLIST, 1) {
376-
st.Logger.Printf(" path %q (local dir %q)", requested, localDir)
354+
st.Logger.Printf(" path %q (local dir %q)", requested, local)
377355
}
378356
// st.Logger.Printf("getRootStrip(requested=%q, localDir=%q", requested, localDir)
379-
rootPath, strip := getRootStrip(requested, localDir)
357+
strip := getStrip(requested)
380358
// st.Logger.Printf("root=%q, strip=%q", root, strip)
381-
prefix := strings.TrimPrefix(rootPath, filepath.Clean(strip))
382-
if prefix != "" {
383-
prefix = strings.TrimPrefix(prefix, "/")
384-
prefix += "/"
385-
}
386359
if st.Opts.DebugGTE(rsyncopts.DEBUG_FLIST, 1) {
387-
st.Logger.Printf(" filepath.Walk(%q), strip=%q", rootPath, strip)
388-
st.Logger.Printf(" prefix=%q", prefix)
360+
st.Logger.Printf(" fs.Walk(%q, %q), strip=%q", local, requested)
389361
}
390362

391363
sw := &scopedWalker{
392-
st: st,
393-
conn: st.Conn,
394-
fec: fec,
395-
excl: excl,
396-
uidMap: uidMap,
397-
gidMap: gidMap,
398-
fileList: &fileList,
399-
prefix: prefix,
400-
ioError: ioError,
401-
rootPath: rootPath,
364+
st: st,
365+
conn: st.Conn,
366+
fec: fec,
367+
excl: excl,
368+
uidMap: uidMap,
369+
gidMap: gidMap,
370+
fileList: &fileList,
371+
ioError: ioError,
372+
localDir: local,
373+
requested: requested,
374+
strip: strip,
402375
}
403376
if err := sw.walk(); err != nil {
404377
return nil, err

internal/sender/flist_test.go

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,47 +5,31 @@ import "testing"
55
func TestPath(t *testing.T) {
66
for _, tt := range []struct {
77
requested string
8-
localDir string
98
wantRoot string
109
wantStrip string
1110
}{
1211
{
13-
requested: "/", // sent by client
14-
localDir: "/usr/share/man", // module.Path
15-
16-
wantRoot: "/usr/share/man",
17-
wantStrip: "/usr/share/man/",
12+
requested: "/", // sent by client
13+
wantStrip: "",
1814
},
1915

2016
{
21-
requested: "tr/man5", // sent by client
22-
localDir: "/usr/share/man", // module.Path
23-
24-
wantRoot: "/usr/share/man/tr/man5",
25-
wantStrip: "/usr/share/man/tr/",
17+
requested: "tr/man5", // sent by client
18+
wantStrip: "",
2619
},
2720

28-
{ // client started with src=/usr/share/man
29-
requested: "", // sent by client
30-
localDir: "/usr/share/man", // module.Path
31-
32-
wantRoot: "/usr/share/man",
33-
wantStrip: "/usr/share/",
21+
{
22+
requested: "tr/", // sent by client
23+
wantStrip: "tr/",
3424
},
3525

36-
{ // client started with src=/usr/share/man/
37-
requested: "/", // sent by client
38-
localDir: "/usr/share/man", // module.Path
39-
40-
wantRoot: "/usr/share/man",
41-
wantStrip: "/usr/share/man/",
26+
{ // client started with src=/usr/share/man
27+
requested: "", // sent by client
28+
wantStrip: "",
4229
},
4330
} {
4431
t.Run("requested="+tt.requested, func(t *testing.T) {
45-
gotRoot, gotStrip := getRootStrip(tt.requested, tt.localDir)
46-
if gotRoot != tt.wantRoot {
47-
t.Errorf("unexpected root: got %q, want %q", gotRoot, tt.wantRoot)
48-
}
32+
gotStrip := getStrip(tt.requested)
4933
if gotStrip != tt.wantStrip {
5034
t.Errorf("unexpected strip: got %q, want %q", gotStrip, tt.wantStrip)
5135
}

0 commit comments

Comments
 (0)