binctr/vendor/github.com/cyphar/filepath-securejoin/join_test.go
Jess Frazelle 639756e8c6 update vendor
Signed-off-by: Jess Frazelle <acidburn@microsoft.com>
2018-03-19 21:36:34 -04:00

385 lines
12 KiB
Go

// Copyright (C) 2017 SUSE LLC. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package securejoin
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"syscall"
"testing"
)
// TODO: These tests won't work on plan9 because it doesn't have symlinks, and
// also we use '/' here explicitly which probably won't work on Windows.
func symlink(t *testing.T, oldname, newname string) {
if err := os.Symlink(oldname, newname); err != nil {
t.Fatal(err)
}
}
// Test basic handling of symlink expansion.
func TestSymlink(t *testing.T) {
dir, err := ioutil.TempDir("", "TestSymlink")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
symlink(t, "somepath", filepath.Join(dir, "etc"))
symlink(t, "../../../../../../../../../../../../../etc", filepath.Join(dir, "etclink"))
symlink(t, "/../../../../../../../../../../../../../etc/passwd", filepath.Join(dir, "passwd"))
for _, test := range []struct {
root, unsafe string
expected string
}{
// Make sure that expansion with a root of '/' proceeds in the expected fashion.
{"/", filepath.Join(dir, "passwd"), "/etc/passwd"},
{"/", filepath.Join(dir, "etclink"), "/etc"},
{"/", filepath.Join(dir, "etc"), filepath.Join(dir, "somepath")},
// Now test scoped expansion.
{dir, "passwd", filepath.Join(dir, "somepath", "passwd")},
{dir, "etclink", filepath.Join(dir, "somepath")},
{dir, "etc", filepath.Join(dir, "somepath")},
{dir, "etc/test", filepath.Join(dir, "somepath", "test")},
{dir, "etc/test/..", filepath.Join(dir, "somepath")},
} {
got, err := SecureJoin(test.root, test.unsafe)
if err != nil {
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
continue
}
// This is only for OS X, where /etc is a symlink to /private/etc. In
// principle, SecureJoin(/, pth) is the same as EvalSymlinks(pth) in
// the case where the path exists.
if test.root == "/" {
if expected, err := filepath.EvalSymlinks(test.expected); err == nil {
test.expected = expected
}
}
if got != test.expected {
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got)
continue
}
}
}
// In a path without symlinks, SecureJoin is equivalent to Clean+Join.
func TestNoSymlink(t *testing.T) {
dir, err := ioutil.TempDir("", "TestNoSymlink")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
for _, test := range []struct {
root, unsafe string
}{
// TODO: Do we need to have some conditional FromSlash handling here?
{dir, "somepath"},
{dir, "even/more/path"},
{dir, "/this/is/a/path"},
{dir, "also/a/../path/././/with/some/./.././junk"},
{dir, "yetanother/../path/././/with/some/./.././junk../../../../../../../../../../../../etc/passwd"},
{dir, "/../../../../../../../../../../../../../../../../etc/passwd"},
{dir, "../../../../../../../../../../../../../../../../somedir"},
{dir, "../../../../../../../../../../../../../../../../"},
{dir, "./../../.././././../../../../../../../../../../../../../../../../etc passwd"},
} {
expected := filepath.Join(test.root, filepath.Clean(string(filepath.Separator)+test.unsafe))
got, err := SecureJoin(test.root, test.unsafe)
if err != nil {
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
continue
}
if got != expected {
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, expected, got)
continue
}
}
}
// Make sure that .. is **not** expanded lexically.
func TestNonLexical(t *testing.T) {
dir, err := ioutil.TempDir("", "TestNonLexical")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
os.MkdirAll(filepath.Join(dir, "subdir"), 0755)
os.MkdirAll(filepath.Join(dir, "cousinparent", "cousin"), 0755)
symlink(t, "../cousinparent/cousin", filepath.Join(dir, "subdir", "link"))
symlink(t, "/../cousinparent/cousin", filepath.Join(dir, "subdir", "link2"))
symlink(t, "/../../../../../../../../../../../../../../../../cousinparent/cousin", filepath.Join(dir, "subdir", "link3"))
for _, test := range []struct {
root, unsafe string
expected string
}{
{dir, "subdir", filepath.Join(dir, "subdir")},
{dir, "subdir/link/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
{dir, "subdir/link2/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
{dir, "subdir/link3/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
{dir, "subdir/../test", filepath.Join(dir, "test")},
// This is the divergence from a simple filepath.Clean implementation.
{dir, "subdir/link/../test", filepath.Join(dir, "cousinparent", "test")},
{dir, "subdir/link2/../test", filepath.Join(dir, "cousinparent", "test")},
{dir, "subdir/link3/../test", filepath.Join(dir, "cousinparent", "test")},
} {
got, err := SecureJoin(test.root, test.unsafe)
if err != nil {
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
continue
}
if got != test.expected {
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got)
continue
}
}
}
// Make sure that symlink loops result in errors.
func TestSymlinkLoop(t *testing.T) {
dir, err := ioutil.TempDir("", "TestSymlinkLoop")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
os.MkdirAll(filepath.Join(dir, "subdir"), 0755)
symlink(t, "../../../../../../../../../../../../../../../../path", filepath.Join(dir, "subdir", "link"))
symlink(t, "/subdir/link", filepath.Join(dir, "path"))
symlink(t, "/../../../../../../../../../../../../../../../../self", filepath.Join(dir, "self"))
for _, test := range []struct {
root, unsafe string
}{
{dir, "subdir/link"},
{dir, "path"},
{dir, "../../path"},
{dir, "subdir/link/../.."},
{dir, "../../../../../../../../../../../../../../../../subdir/link/../../../../../../../../../../../../../../../.."},
{dir, "self"},
{dir, "self/.."},
{dir, "/../../../../../../../../../../../../../../../../self/.."},
{dir, "/self/././.."},
} {
got, err := SecureJoin(test.root, test.unsafe)
if err != ErrSymlinkLoop {
t.Errorf("securejoin(%q, %q): expected ELOOP, got %v & %q", test.root, test.unsafe, err, got)
continue
}
}
}
// Make sure that ENOTDIR is correctly handled.
func TestEnotdir(t *testing.T) {
dir, err := ioutil.TempDir("", "TestEnotdir")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
os.MkdirAll(filepath.Join(dir, "subdir"), 0755)
ioutil.WriteFile(filepath.Join(dir, "notdir"), []byte("I am not a directory!"), 0755)
symlink(t, "/../../../notdir/somechild", filepath.Join(dir, "subdir", "link"))
for _, test := range []struct {
root, unsafe string
}{
{dir, "subdir/link"},
{dir, "notdir"},
{dir, "notdir/child"},
} {
_, err := SecureJoin(test.root, test.unsafe)
if err != nil {
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
continue
}
}
}
// Some silly tests to make sure that all error types are correctly handled.
func TestIsNotExist(t *testing.T) {
for _, test := range []struct {
err error
expected bool
}{
{&os.PathError{Op: "test1", Err: syscall.ENOENT}, true},
{&os.LinkError{Op: "test1", Err: syscall.ENOENT}, true},
{&os.SyscallError{Syscall: "test1", Err: syscall.ENOENT}, true},
{&os.PathError{Op: "test2", Err: syscall.ENOTDIR}, true},
{&os.LinkError{Op: "test2", Err: syscall.ENOTDIR}, true},
{&os.SyscallError{Syscall: "test2", Err: syscall.ENOTDIR}, true},
{&os.PathError{Op: "test3", Err: syscall.EACCES}, false},
{&os.LinkError{Op: "test3", Err: syscall.EACCES}, false},
{&os.SyscallError{Syscall: "test3", Err: syscall.EACCES}, false},
{errors.New("not a proper error"), false},
} {
got := IsNotExist(test.err)
if got != test.expected {
t.Errorf("IsNotExist(%#v): expected %v, got %v", test.err, test.expected, got)
}
}
}
type mockVFS struct {
lstat func(path string) (os.FileInfo, error)
readlink func(path string) (string, error)
}
func (m mockVFS) Lstat(path string) (os.FileInfo, error) { return m.lstat(path) }
func (m mockVFS) Readlink(path string) (string, error) { return m.readlink(path) }
// Make sure that SecureJoinVFS actually does use the given VFS interface.
func TestSecureJoinVFS(t *testing.T) {
dir, err := ioutil.TempDir("", "TestNonLexical")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
os.MkdirAll(filepath.Join(dir, "subdir"), 0755)
os.MkdirAll(filepath.Join(dir, "cousinparent", "cousin"), 0755)
symlink(t, "../cousinparent/cousin", filepath.Join(dir, "subdir", "link"))
symlink(t, "/../cousinparent/cousin", filepath.Join(dir, "subdir", "link2"))
symlink(t, "/../../../../../../../../../../../../../../../../cousinparent/cousin", filepath.Join(dir, "subdir", "link3"))
for _, test := range []struct {
root, unsafe string
expected string
}{
{dir, "subdir", filepath.Join(dir, "subdir")},
{dir, "subdir/link/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
{dir, "subdir/link2/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
{dir, "subdir/link3/test", filepath.Join(dir, "cousinparent", "cousin", "test")},
{dir, "subdir/../test", filepath.Join(dir, "test")},
// This is the divergence from a simple filepath.Clean implementation.
{dir, "subdir/link/../test", filepath.Join(dir, "cousinparent", "test")},
{dir, "subdir/link2/../test", filepath.Join(dir, "cousinparent", "test")},
{dir, "subdir/link3/../test", filepath.Join(dir, "cousinparent", "test")},
} {
var nLstat, nReadlink int
mock := mockVFS{
lstat: func(path string) (os.FileInfo, error) { nLstat++; return os.Lstat(path) },
readlink: func(path string) (string, error) { nReadlink++; return os.Readlink(path) },
}
got, err := SecureJoinVFS(test.root, test.unsafe, mock)
if err != nil {
t.Errorf("securejoin(%q, %q): unexpected error: %v", test.root, test.unsafe, err)
continue
}
if got != test.expected {
t.Errorf("securejoin(%q, %q): expected %q, got %q", test.root, test.unsafe, test.expected, got)
continue
}
if nLstat == 0 && nReadlink == 0 {
t.Errorf("securejoin(%q, %q): expected to use either lstat or readlink, neither were used", test.root, test.unsafe)
}
}
}
// Make sure that SecureJoinVFS actually does use the given VFS interface, and
// that errors are correctly propagated.
func TestSecureJoinVFSErrors(t *testing.T) {
var (
lstatErr = errors.New("lstat error")
readlinkErr = errors.New("readlink err")
)
// Set up directory.
dir, err := ioutil.TempDir("", "TestSecureJoinVFSErrors")
if err != nil {
t.Fatal(err)
}
dir, err = filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Make a link.
symlink(t, "../../../../../../../../../../../../../../../../path", filepath.Join(dir, "link"))
// Define some fake mock functions.
lstatFailFn := func(path string) (os.FileInfo, error) { return nil, lstatErr }
readlinkFailFn := func(path string) (string, error) { return "", readlinkErr }
// Make sure that the set of {lstat, readlink} failures do propagate.
for idx, test := range []struct {
vfs VFS
expected []error
}{
{
expected: []error{nil},
vfs: mockVFS{
lstat: os.Lstat,
readlink: os.Readlink,
},
},
{
expected: []error{lstatErr},
vfs: mockVFS{
lstat: lstatFailFn,
readlink: os.Readlink,
},
},
{
expected: []error{readlinkErr},
vfs: mockVFS{
lstat: os.Lstat,
readlink: readlinkFailFn,
},
},
{
expected: []error{lstatErr, readlinkErr},
vfs: mockVFS{
lstat: lstatFailFn,
readlink: readlinkFailFn,
},
},
} {
_, err := SecureJoinVFS(dir, "link", test.vfs)
success := false
for _, exp := range test.expected {
if err == exp {
success = true
}
}
if !success {
t.Errorf("SecureJoinVFS.mock%d: expected to get lstatError, got %v", idx, err)
}
}
}