Skip to content

Commit 71e8d3b

Browse files
authored
Reject path traversal in filesystem storage (#106)
1 parent d6066af commit 71e8d3b

2 files changed

Lines changed: 55 additions & 12 deletions

File tree

‎internal/storage/filesystem.go‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"path/filepath"
11+
"strings"
1112
"time"
1213
)
1314

@@ -31,12 +32,19 @@ func NewFilesystem(root string) (*Filesystem, error) {
3132
return &Filesystem{root: absRoot}, nil
3233
}
3334

34-
func (fs *Filesystem) fullPath(path string) string {
35-
return filepath.Join(fs.root, filepath.FromSlash(path))
35+
func (fs *Filesystem) fullPath(path string) (string, error) {
36+
full := filepath.Clean(filepath.Join(fs.root, filepath.FromSlash(path)))
37+
if full != fs.root && !strings.HasPrefix(full, fs.root+string(filepath.Separator)) {
38+
return "", fmt.Errorf("%w: path escapes storage root", ErrNotFound)
39+
}
40+
return full, nil
3641
}
3742

3843
func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
39-
fullPath := fs.fullPath(path)
44+
fullPath, err := fs.fullPath(path)
45+
if err != nil {
46+
return 0, "", err
47+
}
4048

4149
dir := filepath.Dir(fullPath)
4250
if err := os.MkdirAll(dir, dirPermissions); err != nil {
@@ -83,7 +91,10 @@ func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int6
8391
}
8492

8593
func (fs *Filesystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
86-
fullPath := fs.fullPath(path)
94+
fullPath, err := fs.fullPath(path)
95+
if err != nil {
96+
return nil, err
97+
}
8798

8899
f, err := os.Open(fullPath)
89100
if err != nil {
@@ -97,9 +108,12 @@ func (fs *Filesystem) Open(ctx context.Context, path string) (io.ReadCloser, err
97108
}
98109

99110
func (fs *Filesystem) Exists(ctx context.Context, path string) (bool, error) {
100-
fullPath := fs.fullPath(path)
111+
fullPath, err := fs.fullPath(path)
112+
if err != nil {
113+
return false, err
114+
}
101115

102-
_, err := os.Stat(fullPath)
116+
_, err = os.Stat(fullPath)
103117
if err != nil {
104118
if os.IsNotExist(err) {
105119
return false, nil
@@ -111,9 +125,12 @@ func (fs *Filesystem) Exists(ctx context.Context, path string) (bool, error) {
111125
}
112126

113127
func (fs *Filesystem) Delete(ctx context.Context, path string) error {
114-
fullPath := fs.fullPath(path)
128+
fullPath, err := fs.fullPath(path)
129+
if err != nil {
130+
return err
131+
}
115132

116-
err := os.Remove(fullPath)
133+
err = os.Remove(fullPath)
117134
if err != nil && !os.IsNotExist(err) {
118135
return fmt.Errorf("removing file: %w", err)
119136
}
@@ -135,7 +152,10 @@ func (fs *Filesystem) SignedURL(_ context.Context, _ string, _ time.Duration) (s
135152
}
136153

137154
func (fs *Filesystem) Size(ctx context.Context, path string) (int64, error) {
138-
fullPath := fs.fullPath(path)
155+
fullPath, err := fs.fullPath(path)
156+
if err != nil {
157+
return 0, err
158+
}
139159

140160
info, err := os.Stat(fullPath)
141161
if err != nil {
@@ -174,7 +194,8 @@ func (fs *Filesystem) Root() string {
174194

175195
// FullPath returns the full filesystem path for a storage path.
176196
// Useful for serving files directly or debugging.
177-
func (fs *Filesystem) FullPath(path string) string {
197+
// Returns an error if the resulting path would escape the storage root.
198+
func (fs *Filesystem) FullPath(path string) (string, error) {
178199
return fs.fullPath(path)
179200
}
180201

‎internal/storage/filesystem_test.go‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ func TestFilesystemStore(t *testing.T) {
5252
}
5353

5454
// Verify file exists on disk
55-
fullPath := fs.FullPath("npm/lodash/4.17.21/lodash.tgz")
55+
fullPath, err := fs.FullPath("npm/lodash/4.17.21/lodash.tgz")
56+
if err != nil {
57+
t.Fatalf("FullPath failed: %v", err)
58+
}
5659
data, err := os.ReadFile(fullPath)
5760
if err != nil {
5861
t.Fatalf("reading stored file: %v", err)
@@ -163,7 +166,10 @@ func TestFilesystemDelete(t *testing.T) {
163166
}
164167

165168
// Empty parent directories should be cleaned up
166-
nestedDir := fs.FullPath("test/delete/nested")
169+
nestedDir, err := fs.FullPath("test/delete/nested")
170+
if err != nil {
171+
t.Fatalf("FullPath failed: %v", err)
172+
}
167173
if _, err := os.Stat(nestedDir); !os.IsNotExist(err) {
168174
t.Error("empty nested directory not cleaned up")
169175
}
@@ -237,6 +243,22 @@ func TestFilesystemLargeFile(t *testing.T) {
237243
assertLargeFileRoundTrip(t, createTestFilesystem(t))
238244
}
239245

246+
func TestFilesystemRejectsTraversal(t *testing.T) {
247+
tmp := t.TempDir()
248+
fs, err := NewFilesystem(tmp)
249+
if err != nil {
250+
t.Fatal(err)
251+
}
252+
for _, p := range []string{"../etc/passwd", "../../etc/passwd", "a/../../etc/passwd"} {
253+
if _, err := fs.Open(context.Background(), p); err == nil {
254+
t.Errorf("Open(%q) should reject traversal", p)
255+
}
256+
if _, _, err := fs.Store(context.Background(), p, strings.NewReader("x")); err == nil {
257+
t.Errorf("Store(%q) should reject traversal", p)
258+
}
259+
}
260+
}
261+
240262
func TestFilesystemSignedURLUnsupported(t *testing.T) {
241263
fs := createTestFilesystem(t)
242264

0 commit comments

Comments
 (0)