Skip to content

Commit b4af172

Browse files
authored
security(storage): reject path-traversal keys in filesystem storage (#1351)
1 parent 04ae270 commit b4af172

3 files changed

Lines changed: 101 additions & 7 deletions

File tree

internal/storage/storage.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import (
77
)
88

99
var (
10-
ErrNotFound = errors.New("file not found")
10+
ErrNotFound = errors.New("file not found")
11+
ErrInvalidStorageKey = errors.New("invalid storage key")
1112
)
1213

1314
type StorageOptions struct {

internal/storage/storage_fs.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,29 @@ func NewFSStorage(root string) (storage Storage, err error) {
3030
return &fsStorage{root: root}, nil
3131
}
3232

33+
// joinRootSafe returns filepath.Join(fs.root, key) only when key is
34+
// filepath.IsLocal, so lexical ".." segments cannot escape the storage root.
35+
func (fs *fsStorage) joinRootSafe(key string) (filename string, err error) {
36+
if key == "" {
37+
return "", errors.New("key is required")
38+
}
39+
if strings.Contains(key, "\x00") {
40+
return "", ErrInvalidStorageKey
41+
}
42+
k := filepath.FromSlash(strings.Trim(filepath.ToSlash(key), "/"))
43+
if !filepath.IsLocal(k) {
44+
return "", ErrInvalidStorageKey
45+
}
46+
root := filepath.Clean(fs.root)
47+
full := filepath.Join(root, k)
48+
return full, nil
49+
}
50+
3351
func (fs *fsStorage) Stat(key string) (stat Stat, err error) {
34-
filename := filepath.Join(fs.root, key)
52+
filename, err := fs.joinRootSafe(key)
53+
if err != nil {
54+
return nil, err
55+
}
3556
fi, err := os.Lstat(filename)
3657
if err != nil {
3758
if os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory") {
@@ -43,7 +64,10 @@ func (fs *fsStorage) Stat(key string) (stat Stat, err error) {
4364
}
4465

4566
func (fs *fsStorage) Get(key string) (content io.ReadCloser, stat Stat, err error) {
46-
filename := filepath.Join(fs.root, key)
67+
filename, err := fs.joinRootSafe(key)
68+
if err != nil {
69+
return
70+
}
4771
file, err := os.Open(filename)
4872
if err != nil && (os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory")) {
4973
err = ErrNotFound
@@ -57,7 +81,10 @@ func (fs *fsStorage) Get(key string) (content io.ReadCloser, stat Stat, err erro
5781
}
5882

5983
func (fs *fsStorage) Put(key string, content io.Reader) (err error) {
60-
filename := filepath.Join(fs.root, key)
84+
filename, err := fs.joinRootSafe(key)
85+
if err != nil {
86+
return err
87+
}
6188
err = ensureDir(filepath.Dir(filename))
6289
if err != nil {
6390
return
@@ -77,24 +104,44 @@ func (fs *fsStorage) Put(key string, content io.Reader) (err error) {
77104
}
78105

79106
func (fs *fsStorage) Delete(key string) (err error) {
80-
return os.Remove(filepath.Join(fs.root, key))
107+
filename, err := fs.joinRootSafe(key)
108+
if err != nil {
109+
return err
110+
}
111+
return os.Remove(filename)
81112
}
82113

83114
func (fs *fsStorage) List(prefix string) (keys []string, err error) {
84115
dir := strings.TrimSuffix(utils.NormalizePathname(prefix)[1:], "/")
85-
return findFiles(filepath.Join(fs.root, dir), dir)
116+
dir = strings.Trim(strings.TrimSpace(dir), "/")
117+
scanRoot := filepath.Clean(fs.root)
118+
parentKey := ""
119+
if dir != "" {
120+
var ferr error
121+
scanRoot, ferr = fs.joinRootSafe(dir)
122+
if ferr != nil {
123+
return nil, ferr
124+
}
125+
parentKey = dir
126+
}
127+
return findFiles(scanRoot, parentKey)
86128
}
87129

88130
func (fs *fsStorage) DeleteAll(prefix string) (deletedKeys []string, err error) {
89131
dir := strings.TrimSuffix(utils.NormalizePathname(prefix)[1:], "/")
132+
dir = strings.Trim(strings.TrimSpace(dir), "/")
90133
if dir == "" {
91134
return nil, errors.New("prefix is required")
92135
}
93136
keys, err := fs.List(prefix)
94137
if err != nil {
95138
return
96139
}
97-
err = os.RemoveAll(filepath.Join(fs.root, dir))
140+
absDir, err := fs.joinRootSafe(dir)
141+
if err != nil {
142+
return nil, err
143+
}
144+
err = os.RemoveAll(absDir)
98145
if err != nil {
99146
return
100147
}

internal/storage/storage_fs_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,49 @@ func TestFSStorage(t *testing.T) {
115115
t.Fatalf("invalid keys count(%d), shoud be 0", len(keys))
116116
}
117117
}
118+
119+
func TestFSStorageRejectPathTraversal(t *testing.T) {
120+
root := path.Join(os.TempDir(), "storage_traversal_"+rand.Hex.String(8))
121+
fs, err := NewFSStorage(root)
122+
if err != nil {
123+
t.Fatal(err)
124+
}
125+
defer os.RemoveAll(root)
126+
127+
attackKeys := []string{
128+
"../outside.txt",
129+
"legacy/../../../tmp/pwned",
130+
`legacy/v111/react@19.2.0/esnext/../../../gh/a/exp@cafe/foo.md#/../../../../../../../../../../tmp/pwned`,
131+
"safe/../../etc/passwd",
132+
"bad\x00surprise",
133+
}
134+
for _, k := range attackKeys {
135+
err = fs.Put(k, bytes.NewBufferString("evil"))
136+
if err != ErrInvalidStorageKey {
137+
t.Fatalf("Put(%q): want ErrInvalidStorageKey, got %v", k, err)
138+
}
139+
if _, err = fs.Stat(k); err != ErrInvalidStorageKey {
140+
t.Fatalf("Stat(%q): want ErrInvalidStorageKey, got %v", k, err)
141+
}
142+
if _, _, err = fs.Get(k); err != ErrInvalidStorageKey {
143+
t.Fatalf("Get(%q): want ErrInvalidStorageKey, got %v", k, err)
144+
}
145+
if err = fs.Delete(k); err != ErrInvalidStorageKey {
146+
t.Fatalf("Delete(%q): want ErrInvalidStorageKey, got %v", k, err)
147+
}
148+
}
149+
150+
err = fs.Put("ok/sub/file.txt", bytes.NewBufferString("hi"))
151+
if err != nil {
152+
t.Fatal(err)
153+
}
154+
got, _, err := fs.Get("ok/sub/file.txt")
155+
if err != nil {
156+
t.Fatal(err)
157+
}
158+
defer got.Close()
159+
b, err := io.ReadAll(got)
160+
if err != nil || string(b) != "hi" {
161+
t.Fatalf("Get ok path: content %q err %v", string(b), err)
162+
}
163+
}

0 commit comments

Comments
 (0)