From 42fea1b5e4934126aff8a71ad084b27667b959b7 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Fri, 15 Apr 2022 14:38:15 +0200 Subject: [PATCH] kustomize: introduce secure FS implementation This implementation functions as a drop-in replacement for Kustomize's own `fsOnDisk`, and asserts any path it handles to be inside root. In essence, the whole file system is now restricted like loader.RestrictionRootOnly would, but while allowing root to differ from the top Kustomization directory. The main reason to put the constraint in the file system implementation is because the current Krusty API does not allow to configure a custom load restrictor, but does allow injecting a custom FS. Signed-off-by: Hidde Beydals --- kustomize/filesys/fs_secure.go | 229 ++++++++++++ kustomize/filesys/fs_secure_test.go | 551 ++++++++++++++++++++++++++++ 2 files changed, 780 insertions(+) create mode 100644 kustomize/filesys/fs_secure.go create mode 100644 kustomize/filesys/fs_secure_test.go diff --git a/kustomize/filesys/fs_secure.go b/kustomize/filesys/fs_secure.go new file mode 100644 index 000000000..68d9395c2 --- /dev/null +++ b/kustomize/filesys/fs_secure.go @@ -0,0 +1,229 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesys + +import ( + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +// MakeFsOnDiskSecure returns a secure file system which asserts any paths it +// handles to be inside root. +func MakeFsOnDiskSecure(root string) (filesys.FileSystem, error) { + unsafeFS := filesys.MakeFsOnDisk() + cleanedAbs, _, err := unsafeFS.CleanedAbs(root) + if err != nil { + return nil, err + } + return fsSecure{root: cleanedAbs, unsafeFS: unsafeFS}, nil +} + +// fsSecure wraps an unsafe FileSystem implementation, and secures it +// by confirming paths are inside root. +type fsSecure struct { + root filesys.ConfirmedDir + unsafeFS filesys.FileSystem +} + +// ConstraintError records an error and the operation and file that +// violated it. +type ConstraintError struct { + Op string + Path string + Err error +} + +func (e *ConstraintError) Error() string { + return "fs-security-constraint " + e.Op + " " + e.Path + ": " + e.Err.Error() +} + +func (e *ConstraintError) Unwrap() error { return e.Err } + +// Create delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) Create(path string) (filesys.File, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "create", Path: path, Err: err} + } + return fs.unsafeFS.Create(path) +} + +// Mkdir delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) Mkdir(path string) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "mkdir", Path: path, Err: err} + } + return fs.unsafeFS.Mkdir(path) +} + +// MkdirAll delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// type ConstraintError is returned. +func (fs fsSecure) MkdirAll(path string) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "mkdir", Path: path, Err: err} + } + return fs.unsafeFS.MkdirAll(path) +} + +// RemoveAll delegates to the embedded unsafe FS after having confirmed the +// path to be inside root. If the provided path violates this constraint, an +// error of type ConstraintError is returned. +func (fs fsSecure) RemoveAll(path string) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "remove", Path: path, Err: err} + } + return fs.unsafeFS.RemoveAll(path) +} + +// Open delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) Open(path string) (filesys.File, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "open", Path: path, Err: err} + } + return fs.unsafeFS.Open(path) +} + +// IsDir delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, it returns +// false. +func (fs fsSecure) IsDir(path string) bool { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return false + } + return fs.unsafeFS.IsDir(path) +} + +// ReadDir delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) ReadDir(path string) ([]string, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "open", Path: path, Err: err} + } + return fs.unsafeFS.ReadDir(path) +} + +// CleanedAbs delegates to the embedded unsafe FS, but confirms the returned +// result to be within root. If the results violates this constraint, an error +// of type ConstraintError is returned. +// In essence, it functions the same as Kustomize's loader.RestrictionRootOnly, +// but on FS levels, and while allowing file paths. +func (fs fsSecure) CleanedAbs(path string) (filesys.ConfirmedDir, string, error) { + d, f, err := fs.unsafeFS.CleanedAbs(path) + if err != nil { + return d, f, err + } + if !d.HasPrefix(fs.root) { + err := fmt.Errorf("file '%s' is not in or below '%s'", path, fs.root) + return "", "", &ConstraintError{Op: "abs", Path: path, Err: err} + } + return d, f, err +} + +// Exists delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, it returns +// false. +func (fs fsSecure) Exists(path string) bool { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return false + } + return fs.unsafeFS.Exists(path) +} + +// Glob delegates to the embedded unsafe FS, but filters the returned paths to +// only include items inside root. +func (fs fsSecure) Glob(pattern string) ([]string, error) { + paths, err := fs.unsafeFS.Glob(pattern) + if err != nil { + return nil, err + } + var securePaths []string + for _, p := range paths { + if err := isSecurePath(fs.unsafeFS, fs.root, p); err == nil { + securePaths = append(securePaths, p) + } + } + return securePaths, err +} + +// ReadFile delegates to the embedded unsafe FS after having confirmed the path +// to be inside root. If the provided path violates this constraint, an error +// of type ConstraintError is returned. +func (fs fsSecure) ReadFile(path string) ([]byte, error) { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return nil, &ConstraintError{Op: "read", Path: path, Err: err} + } + return fs.unsafeFS.ReadFile(path) +} + +// WriteFile delegates to the embedded unsafe FS after having confirmed the +// path to be inside root. If the provided path violates this constraint, an +// error of type ConstraintError is returned. +func (fs fsSecure) WriteFile(path string, data []byte) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "write", Path: path, Err: err} + } + return fs.unsafeFS.WriteFile(path, data) +} + +// Walk delegates to the embedded unsafe FS, wrapping falkFn in a callback which +// confirms the path to be inside root. If the path violates this constraint, +// an error of type ConstraintError is returned and walkFn is not called. +func (fs fsSecure) Walk(path string, walkFn filepath.WalkFunc) error { + wrapWalkFn := func(path string, info os.FileInfo, err error) error { + if err := isSecurePath(fs.unsafeFS, fs.root, path); err != nil { + return &ConstraintError{Op: "walk", Path: path, Err: err} + } + return walkFn(path, info, err) + } + return fs.unsafeFS.Walk(path, wrapWalkFn) +} + +// isSecurePath confirms the given path is inside root using the provided file +// system. At present, it assumes the file system implementation to be on disk +// and makes use of filepath.EvalSymlinks. +func isSecurePath(fs filesys.FileSystem, root filesys.ConfirmedDir, path string) error { + absRoot, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("abs path error on '%s': %v", path, err) + } + d := filesys.ConfirmedDir(filepath.Dir(absRoot)) + if fs.Exists(absRoot) { + evaluated, err := filepath.EvalSymlinks(absRoot) + if err != nil { + return fmt.Errorf("evalsymlink failure on '%s': %w", path, err) + } + evaluatedDir := evaluated + if !fs.IsDir(evaluatedDir) { + evaluatedDir = filepath.Dir(evaluatedDir) + } + d = filesys.ConfirmedDir(evaluatedDir) + } + if !d.HasPrefix(root) { + return fmt.Errorf("'%s' is not in or below '%s'", path, root) + } + return nil +} diff --git a/kustomize/filesys/fs_secure_test.go b/kustomize/filesys/fs_secure_test.go new file mode 100644 index 000000000..65e46be44 --- /dev/null +++ b/kustomize/filesys/fs_secure_test.go @@ -0,0 +1,551 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package filesys + +import ( + "bytes" + "io" + "math/rand" + "os" + "path/filepath" + "strings" + "testing" + + . "github.com/onsi/gomega" + "github.com/onsi/gomega/types" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +func Test_fsSecure_Create(t *testing.T) { + g := NewWithT(t) + + root := t.TempDir() + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure create", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + got, err := fs.Create(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(got).ToNot(BeNil()) + g.Expect(got.Close()).To(Succeed()) + g.Expect(fs.Exists(path)).To(BeTrue()) + }) + + t.Run("illegal create", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "../file.txt") + got, err := fs.Create("/file.txt") + g.Expect(err).To(HaveOccurred()) + g.Expect(got).To(BeNil()) + g.Expect(fs.Exists(path)).To(BeFalse()) + }) +} + +func Test_fsSecure_Mkdir(t *testing.T) { + g := NewWithT(t) + + root := t.TempDir() + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure mkdir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "secure") + g.Expect(fs.Mkdir(path)).To(Succeed()) + g.Expect(path).To(BeADirectory()) + }) + + t.Run("illegal mkdir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(os.TempDir(), "illegal") + g.Expect(fs.Mkdir(path)).To(HaveOccurred()) + g.Expect(path).ToNot(BeADirectory()) + g.Expect(path).ToNot(BeAnExistingFile()) + }) +} + +func Test_fsSecure_MkdirAll(t *testing.T) { + g := NewWithT(t) + + root := t.TempDir() + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure mkdir all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "secure", "subdir") + g.Expect(fs.MkdirAll(path)).To(Succeed()) + g.Expect(path).To(BeADirectory()) + }) + + t.Run("illegal mkdir all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "..", "..", "subdir") + g.Expect(fs.MkdirAll(path)).To(HaveOccurred()) + g.Expect(path).ToNot(BeADirectory()) + g.Expect(path).ToNot(BeAnExistingFile()) + }) +} + +func Test_fsSecure_RemoveAll(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + root := filepath.Join(tmpDir, "workdir") + + g.Expect(os.MkdirAll(filepath.Join(root, "subdir"), 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "subdir", "file.txt"), []byte(""), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte(""), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure remove all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "subdir") + g.Expect(fs.RemoveAll(path)).To(Succeed()) + g.Expect(path).NotTo(BeADirectory()) + }) + + t.Run("illegal remove all", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + g.Expect(fs.RemoveAll(path)).To(HaveOccurred()) + g.Expect(path).To(BeAnExistingFile()) + }) +} + +func Test_fsSecure_Open(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure open", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + f, err := fs.Open(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(f).ToNot(BeNil()) + var b bytes.Buffer + _, err = io.Copy(&b, f) + g.Expect(err).To(Succeed()) + g.Expect(b.String()).To(Equal("secure")) + }) + + t.Run("illegal open", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + f, err := fs.Open(path) + g.Expect(err).To(HaveOccurred()) + g.Expect(f).To(BeNil()) + }) +} + +func Test_fsSecure_IsDir(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.Mkdir(filepath.Join(tmpDir, "illegal"), 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure is dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "") + g.Expect(fs.IsDir(path)).To(BeTrue()) + }) + + t.Run("illegal is dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "illegal") + g.Expect(fs.IsDir(path)).To(BeFalse()) + }) +} + +func Test_fsSecure_ReadDir(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.Mkdir(filepath.Join(tmpDir, "illegal"), 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "illegal", "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure read dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "") + files, err := fs.ReadDir(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(files).To(HaveLen(1)) + g.Expect(files).To(ContainElement("file.txt")) + }) + + t.Run("illegal is dir", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "illegal") + files, err := fs.ReadDir(path) + g.Expect(err).To(HaveOccurred()) + g.Expect(files).To(HaveLen(0)) + }) +} + +func Test_fsSecure_CleanedAbs(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure cleaned abs", func(t *testing.T) { + g := NewWithT(t) + + d, f, err := fs.CleanedAbs(filepath.Join(root, "../workdir")) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(d).To(Equal(filesys.ConfirmedDir(root))) + g.Expect(f).To(BeEmpty()) + }) + + t.Run("illegal cleaned abs", func(t *testing.T) { + g := NewWithT(t) + + d, f, err := fs.CleanedAbs(filepath.Join(root, "../../workdir")) + g.Expect(err).To(HaveOccurred()) + g.Expect(d).To(BeEmpty()) + g.Expect(f).To(BeEmpty()) + }) +} + +func Test_fsSecure_Exists(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure exists", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(fs.Exists(root)).To(BeTrue()) + }) + + t.Run("illegal exists", func(t *testing.T) { + g := NewWithT(t) + + g.Expect(fs.Exists(tmpDir)).To(BeFalse()) + }) +} + +func Test_fsSecure_Glob(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + files, err := fs.Glob(filepath.Join(tmpDir, "*/*.txt")) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(files).To(ContainElement(filepath.Join(root, "file.txt"))) + g.Expect(files).ToNot(ContainElement(filepath.Join(tmpDir, "file.txt"))) +} + +func Test_fsSecure_ReadFile(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure read file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + b, err := fs.ReadFile(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(b).To(Equal([]byte("secure"))) + }) + + t.Run("illegal read file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + b, err := fs.ReadFile(path) + g.Expect(err).To(HaveOccurred()) + g.Expect(b).To(BeNil()) + }) +} + +func Test_fsSecure_WriteFile(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure write file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(root, "file.txt") + data := []byte("secure") + err := fs.WriteFile(path, data) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(path).To(BeAnExistingFile()) + b, err := fs.ReadFile(path) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(b).To(Equal(data)) + }) + + t.Run("illegal write file", func(t *testing.T) { + g := NewWithT(t) + + path := filepath.Join(tmpDir, "file.txt") + err := fs.WriteFile(path, []byte("illegal")) + g.Expect(err).To(HaveOccurred()) + g.Expect(path).ToNot(BeAnExistingFile()) + }) +} + +func Test_fsSecure_Walk(t *testing.T) { + g := NewWithT(t) + + tmpDir := t.TempDir() + + root := filepath.Join(tmpDir, "workdir") + g.Expect(os.Mkdir(root, 0o700)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(root, "file.txt"), []byte("secure"), 0o644)).To(Succeed()) + g.Expect(os.WriteFile(filepath.Join(tmpDir, "file.txt"), []byte("illegal"), 0o644)).To(Succeed()) + + fs, err := MakeFsOnDiskSecure(root) + g.Expect(err).ToNot(HaveOccurred()) + + t.Run("secure walk", func(t *testing.T) { + g := NewWithT(t) + + var walkedPaths []string + walk := func(path string, info os.FileInfo, err error) error { + walkedPaths = append(walkedPaths, path) + return nil + } + g.Expect(fs.Walk(root, walk)).To(Succeed()) + g.Expect(walkedPaths).To(Equal([]string{root, filepath.Join(root, "file.txt")})) + }) + + t.Run("illegal walk", func(t *testing.T) { + g := NewWithT(t) + + var walkedPaths []string + walk := func(path string, info os.FileInfo, err error) error { + walkedPaths = append(walkedPaths, path) + return nil + } + g.Expect(fs.Walk(tmpDir, walk)).To(HaveOccurred()) + g.Expect(walkedPaths).To(BeEmpty()) + }) +} + +func Test_isSecurePath(t *testing.T) { + type file struct { + name string + symlink string + } + tests := []struct { + name string + fs filesys.FileSystem + rootSuffix string + files []file + path string + wantErr types.GomegaMatcher + }{ + { + name: "secure non existing path", + fs: filesys.MakeFsOnDisk(), + path: "/filepath", + wantErr: Succeed(), + }, + { + name: "illegal relative path", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + path: "../", + wantErr: HaveOccurred(), + }, + { + name: "illegal absolute path", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + path: "", + wantErr: HaveOccurred(), + }, + { + name: "relative symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "subdir/file.txt"}, + {name: "subdir/subsubdir/symlink", symlink: "../file.txt"}, + }, + path: "/subdir/subsubdir/symlink", + wantErr: Succeed(), + }, + { + name: "absolute symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "subdir/file.txt"}, + {name: "subdir/subsubdir/symlink", symlink: "/subdir/file.txt"}, + }, + path: "/subdir/subsubdir/symlink", + wantErr: Succeed(), + }, + { + name: "illegal relative symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "file.txt"}, + {name: "subdir/symlink", symlink: "../file.txt"}, + }, + path: "/subdir/symlink", + wantErr: HaveOccurred(), + }, + { + name: "illegal absolute symlink", + fs: filesys.MakeFsOnDisk(), + rootSuffix: "subdir", + files: []file{ + {name: "file.txt"}, + {name: "subdir/symlink", symlink: "/file.txt"}, + }, + path: "/subdir/symlink", + wantErr: HaveOccurred(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + root := newTemp() + realRoot := filesys.ConfirmedDir(filepath.Join(root, tt.rootSuffix)) + g.Expect(tt.fs.MkdirAll(realRoot.String())).To(Succeed()) + t.Cleanup(func() { + g.Expect(tt.fs.RemoveAll(root)).To(Succeed()) + }) + + for _, f := range tt.files { + fPath := filepath.Join(root, f.name) + dir, base := filepath.Split(fPath) + g.Expect(tt.fs.MkdirAll(dir)).To(Succeed()) + + if symlink := f.symlink; symlink != "" { + if strings.HasPrefix(symlink, "") { + symlink = strings.Replace(symlink, "", root, 1) + } + g.Expect(os.Symlink(symlink, fPath)).To(Succeed()) + continue + } + + if base != "" { + file, err := tt.fs.Create(fPath) + g.Expect(err).ToNot(HaveOccurred()) + _, err = file.Write([]byte(f.name + " data")) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(file.Close()).To(Succeed()) + } + } + + path := tt.path + if strings.HasPrefix(path, "") { + path = strings.Replace(path, "", root, 1) + } + + err := isSecurePath(tt.fs, realRoot, path) + g.Expect(err).To(tt.wantErr) + }) + } +} + +func newTemp() string { + return filepath.Join(os.TempDir(), "securefs-"+randStringBytes(5)) +} + +func randStringBytes(n int) string { + const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))] + } + return string(b) +}