Skip to content

Commit

Permalink
Merge pull request #138 from benmoss/invalid-symlinks
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Moss authored Mar 4, 2022
2 parents a9dbae3 + e798337 commit 59d961e
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 1 deletion.
10 changes: 9 additions & 1 deletion pkg/vendir/cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ type SyncOptions struct {
Directories []string
Locked bool

Chdir string
Chdir string
AllowAllSymlinkDestinations bool
}

func NewSyncOptions(ui ui.UI) *SyncOptions {
Expand All @@ -50,6 +51,7 @@ func NewSyncCmd(o *SyncOptions) *cobra.Command {
cmd.Flags().BoolVarP(&o.Locked, "locked", "l", false, "Consult lock file to pull exact references (e.g. use git sha instead of branch name)")

cmd.Flags().StringVar(&o.Chdir, "chdir", "", "Set current directory for process")
cmd.Flags().BoolVar(&o.AllowAllSymlinkDestinations, "dangerous-allow-all-symlink-destinations", false, "Symlinks to all destinations are allowed")

return cmd
}
Expand Down Expand Up @@ -126,6 +128,12 @@ func (o *SyncOptions) Run() error {
if err != nil {
return fmt.Errorf("Syncing directory '%s': %s", dirConf.Path, err)
}
if !o.AllowAllSymlinkDestinations {
err = ctldir.ValidateSymlinks(dirConf.Path)
if err != nil {
return err
}
}

newLockConfig.Directories = append(newLockConfig.Directories, dirLockConf)
}
Expand Down
45 changes: 45 additions & 0 deletions pkg/vendir/directory/symlink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

package directory

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)

// ValidateSymlinks enforces that symlinks inside the given path resolve to inside the path
func ValidateSymlinks(path string) error {
absRoot, err := filepath.Abs(path)
if err != nil {
return err
}
rootSegments := strings.Split(absRoot, string(os.PathSeparator))
return filepath.WalkDir(path, func(path string, info fs.DirEntry, err error) error {
if info.Type()&os.ModeSymlink == os.ModeSymlink {
resolvedPath, err := filepath.EvalSymlinks(path)
if err != nil {
return fmt.Errorf("Unable to resolve symlink: %w", err)
}
absPath, err := filepath.Abs(resolvedPath)
if err != nil {
return err
}
pathSegments := strings.Split(absPath, string(os.PathSeparator))

if len(rootSegments) > len(pathSegments) {
return fmt.Errorf("Invalid symlink found to outside parent directory: %q", absPath)
}
for i, segment := range rootSegments {
if pathSegments[i] != segment {
return fmt.Errorf("Invalid symlink found to outside parent directory: %q", absPath)
}
}
}
return nil
})

}
97 changes: 97 additions & 0 deletions pkg/vendir/directory/symlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

package directory

import (
"os"
"path/filepath"
"testing"
)

func TestValidateSymlinks(t *testing.T) {
root, err := os.MkdirTemp("", "vendir-test")
if err != nil {
t.Fatalf("failed to create tmpdir: %v", err)
}
wd := filepath.Join(root, "wd")
validFilePath := filepath.Join(wd, "file")

sibling := filepath.Join(root, "wd2")
siblingFilePath := filepath.Join(sibling, "file")

for _, path := range []string{wd, sibling} {
if err = os.Mkdir(path, os.ModePerm); err != nil {
t.Fatalf("failed to create dir: %v", err)
}
}
for _, path := range []string{validFilePath, siblingFilePath} {
file, err := os.Create(path)
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
file.Close()
}
defer os.RemoveAll(root)

tests := []struct {
name string
target string
wantErr bool
}{
{
name: "valid symlink",
target: validFilePath,
wantErr: false,
},
{
name: "valid symlink to containing directory",
target: wd,
wantErr: false,
},
{
name: "invalid symlink",
target: siblingFilePath,
wantErr: true,
},
{
name: "invalid symlink to sibling directory",
target: sibling,
wantErr: true,
},
{
name: "invalid symlink to parent directory",
target: root,
wantErr: true,
},
{
name: "invalid symlink to non-existent path",
target: filepath.Join(wd, "foo"),
wantErr: true,
},
}
for _, tt := range tests {
test := func(t *testing.T) {
newName := filepath.Join(wd, "symlink")
t.Logf("target: %q\n", tt.target)
err := os.Symlink(tt.target, newName)
if err != nil {
t.Fatalf("creating symlink: %v", err)
}
defer os.Remove(newName)
if err := ValidateSymlinks(wd); (err != nil) != tt.wantErr {
t.Errorf("ValidateSymlinks() error = %v, wantErr %v", err, tt.wantErr)
}
}

t.Run(tt.name+" absolute", test)
t.Run(tt.name+" relative", func(t *testing.T) {
oldName, err := filepath.Rel(wd, tt.target)
if err != nil {
t.Fatalf("relativizing path: %v", err)
}
tt.target = oldName
test(t)
})
}
}
107 changes: 107 additions & 0 deletions test/e2e/invalid_symlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: Apache-2.0

package e2e

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/vmware-tanzu/carvel-vendir/pkg/vendir/config"
"gopkg.in/yaml.v2"
)

func TestInvalidSymlink(t *testing.T) {
env := BuildEnv(t)
vendir := Vendir{t, env.BinaryPath, Logger{}}

tmpDir, err := os.MkdirTemp("", "vendir-test")
if err != nil {
t.Fatalf("creating tmpdir: %v", err)
}
defer os.RemoveAll(tmpDir)

symlinkDir := filepath.Join(tmpDir, "symlink-dir")
err = os.Mkdir(symlinkDir, os.ModePerm)
if err != nil {
t.Fatalf("creating symlink dir: %v", err)

}

// valid since it is in the symlink-dir
validFilePath := filepath.Join(symlinkDir, "a_valid_file.txt")
validFile, err := os.Create(validFilePath)
if err != nil {
t.Fatalf("creating file: %v", err)
}
validFile.Close()

//invalid since it is outside the symlink-dir
invalidFilePath := filepath.Join(tmpDir, "invalid_file.txt")
invalidFile, err := os.Create(invalidFilePath)
if err != nil {
t.Fatalf("creating file: %v", err)
}
invalidFile.Close()

config := config.Config{
APIVersion: "vendir.k14s.io/v1alpha1",
Kind: "Config",
Directories: []config.Directory{{
Path: "result",
Contents: []config.DirectoryContents{{
Path: "bad",
Directory: &config.DirectoryContentsDirectory{
Path: "symlink-dir",
},
}},
}},
}
vendirYML, err := os.Create(filepath.Join(tmpDir, "vendir.yml"))
if err != nil {
t.Fatalf("creating vendir.yml: %v", err)
}
defer vendirYML.Close()

err = yaml.NewEncoder(vendirYML).Encode(&config)
if err != nil {
t.Fatalf("writing vendir.yml: %v", err)
}

tests := []struct {
symlinkLocation string
valid bool
expectedErr string
}{
{symlinkLocation: "a_valid_file.txt", valid: true},
{symlinkLocation: invalidFilePath, valid: false, expectedErr: "Invalid symlink found to outside parent directory"},
{symlinkLocation: "non_existent_file.txt", valid: false, expectedErr: "Unable to resolve symlink"},
}
for _, tc := range tests {
symlinkPath := filepath.Join(symlinkDir, "file")
err = os.Symlink(tc.symlinkLocation, symlinkPath)
if err != nil {
t.Fatalf("creating symlink: %v", err)
}

_, err = vendir.RunWithOpts([]string{"sync"}, RunOpts{Dir: tmpDir, AllowError: true})
if tc.valid && err != nil {
t.Fatalf("unexpected err: %v", err)
}
if !tc.valid {
if err == nil {
t.Fatalf("expected an err, got none")
}
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Fatalf("Expected invalid symlink err: %s", err)
}
}

err = os.Remove(symlinkPath)
if err != nil {
t.Fatalf("deleting symlink: %v", err)
}
}
}

0 comments on commit 59d961e

Please sign in to comment.