Skip to content

Commit 18c711b

Browse files
authored
Merge pull request #195 from git-pkgs/fix-filesystem-storage-path-validation
Validate filesystem storage paths
2 parents 95bb9d8 + 98150ea commit 18c711b

2 files changed

Lines changed: 83 additions & 12 deletions

File tree

internal/storage/filesystem.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,31 @@ func NewFilesystem(root string) (*Filesystem, error) {
3434
}
3535

3636
func (fs *Filesystem) fullPath(path string) (string, error) {
37-
full := filepath.Clean(filepath.Join(fs.root, filepath.FromSlash(path)))
38-
if full != fs.root && !strings.HasPrefix(full, fs.root+string(filepath.Separator)) {
39-
return "", fmt.Errorf("%w: path escapes storage root", ErrNotFound)
37+
localPath, err := cleanStoragePath(path)
38+
if err != nil {
39+
return "", err
40+
}
41+
return filepath.Join(fs.root, localPath), nil
42+
}
43+
44+
func (fs *Filesystem) prefixPath(prefix string) (string, error) {
45+
if prefix == "" {
46+
return fs.root, nil
47+
}
48+
return fs.fullPath(prefix)
49+
}
50+
51+
func cleanStoragePath(path string) (string, error) {
52+
if path == "." || strings.Contains(path, `\`) || !filepath.IsLocal(path) {
53+
return "", fmt.Errorf("%w: invalid storage path", ErrNotFound)
4054
}
41-
return full, nil
55+
56+
localPath, err := filepath.Localize(path)
57+
if err != nil || localPath == "." || !filepath.IsLocal(localPath) {
58+
return "", fmt.Errorf("%w: invalid storage path", ErrNotFound)
59+
}
60+
61+
return localPath, nil
4262
}
4363

4464
func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
@@ -190,7 +210,7 @@ func (fs *Filesystem) UsedSpace(ctx context.Context) (int64, error) {
190210

191211
// ListPrefix returns object metadata for paths under a prefix.
192212
func (fs *Filesystem) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) {
193-
searchRoot, err := fs.fullPath(prefix)
213+
searchRoot, err := fs.prefixPath(prefix)
194214
if err != nil {
195215
return nil, err
196216
}

internal/storage/filesystem_test.go

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -243,19 +243,70 @@ func TestFilesystemLargeFile(t *testing.T) {
243243
assertLargeFileRoundTrip(t, createTestFilesystem(t))
244244
}
245245

246-
func TestFilesystemRejectsTraversal(t *testing.T) {
246+
func TestFilesystemRejectsInvalidPaths(t *testing.T) {
247247
tmp := t.TempDir()
248248
fs, err := NewFilesystem(tmp)
249249
if err != nil {
250250
t.Fatal(err)
251251
}
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)
252+
for _, p := range []string{
253+
"",
254+
".",
255+
"../etc/passwd",
256+
"../../etc/passwd",
257+
"a/../../etc/passwd",
258+
"/etc/passwd",
259+
"test//file.txt",
260+
"test/./file.txt",
261+
"test/../file.txt",
262+
`test\..\file.txt`,
263+
} {
264+
name := p
265+
if name == "" {
266+
name = "empty"
258267
}
268+
269+
t.Run(name, func(t *testing.T) {
270+
ctx := context.Background()
271+
272+
if _, err := fs.FullPath(p); !errors.Is(err, ErrNotFound) {
273+
t.Errorf("FullPath(%q) = %v, want ErrNotFound", p, err)
274+
}
275+
if _, err := fs.Open(ctx, p); err == nil {
276+
t.Errorf("Open(%q) should reject invalid path", p)
277+
}
278+
if _, _, err := fs.Store(ctx, p, strings.NewReader("x")); err == nil {
279+
t.Errorf("Store(%q) should reject invalid path", p)
280+
}
281+
if _, err := fs.Exists(ctx, p); err == nil {
282+
t.Errorf("Exists(%q) should reject invalid path", p)
283+
}
284+
if err := fs.Delete(ctx, p); err == nil {
285+
t.Errorf("Delete(%q) should reject invalid path", p)
286+
}
287+
if _, err := fs.Size(ctx, p); err == nil {
288+
t.Errorf("Size(%q) should reject invalid path", p)
289+
}
290+
if _, err := fs.ListPrefix(ctx, p); p != "" && err == nil {
291+
t.Errorf("ListPrefix(%q) should reject invalid path", p)
292+
}
293+
})
294+
}
295+
}
296+
297+
func TestFilesystemListPrefixAllowsEmptyPrefix(t *testing.T) {
298+
fs := createTestFilesystem(t)
299+
ctx := context.Background()
300+
301+
_, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa"))
302+
_, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc"))
303+
304+
objects, err := fs.ListPrefix(ctx, "")
305+
if err != nil {
306+
t.Fatalf("ListPrefix empty prefix failed: %v", err)
307+
}
308+
if len(objects) != 2 {
309+
t.Fatalf("ListPrefix empty prefix returned %d objects, want 2", len(objects))
259310
}
260311
}
261312

0 commit comments

Comments
 (0)