diff --git a/kustomize/filesys/fs_secure.go b/kustomize/filesys/fs_secure.go new file mode 100644 index 00000000..05f30811 --- /dev/null +++ b/kustomize/filesys/fs_secure.go @@ -0,0 +1,232 @@ +/* +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) { + return "", "", &ConstraintError{Op: "abs", Path: path, Err: rootConstraintErr(path, fs.root.String())} + } + 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 rootConstraintErr(path, root.String()) + } + return nil +} + +func rootConstraintErr(path, root string) error { + return fmt.Errorf("path '%s' is not in or below '%s'", path, root) +} diff --git a/kustomize/filesys/fs_secure_test.go b/kustomize/filesys/fs_secure_test.go new file mode 100644 index 00000000..65e46be4 --- /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) +}