containerd/archive/path_test.go
Derek McGowan 4a3f98cb61
Add link breakout checks and tests
Ensure symlinks cannot be used to breakout of unpack directory.
Evaluate absolute symlinks as scoped to unpack directory.
Allow symlinks which point outside the root to be created.
Scope all resolution of symlinks to the unpack directory.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
2017-07-18 12:16:13 -07:00

294 lines
6.8 KiB
Go

package archive
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/containerd/containerd/fs/fstest"
"github.com/pkg/errors"
)
type rootCheck struct {
unresolved string
expected string
scope func(string) string
cause error
}
func TestRootPath(t *testing.T) {
tests := []struct {
name string
apply fstest.Applier
checks []rootCheck
scope func(string) (string, error)
}{
{
name: "SymlinkAbsolute",
apply: Symlink("/b", "fs/a/d"),
checks: Check("fs/a/d/c/data", "b/c/data"),
},
{
name: "SymlinkRelativePath",
apply: Symlink("a", "fs/i"),
checks: Check("fs/i", "fs/a"),
},
{
name: "SymlinkSkipSymlinksOutsideScope",
apply: Symlink("realdir", "linkdir"),
checks: CheckWithScope("foo/bar", "foo/bar", "linkdir"),
},
{
name: "SymlinkLastLink",
apply: Symlink("/b", "fs/a/d"),
checks: Check("fs/a/d", "b"),
},
{
name: "SymlinkRelativeLinkChangeScope",
apply: Symlink("../b", "fs/a/e"),
checks: CheckAll(
Check("fs/a/e/c/data", "fs/b/c/data"),
CheckWithScope("e", "b", "fs/a"), // Original return
),
},
{
name: "SymlinkDeepRelativeLinkChangeScope",
apply: Symlink("../../../../test", "fs/a/f"),
checks: CheckAll(
Check("fs/a/f", "test"), // Original return
CheckWithScope("a/f", "test", "fs"), // Original return
),
},
{
name: "SymlinkRelativeLinkChain",
apply: fstest.Apply(
Symlink("../g", "fs/b/h"),
fstest.Symlink("../../../../../../../../../../../../root", "fs/g"),
),
checks: Check("fs/b/h", "root"),
},
{
name: "SymlinkBreakoutPath",
apply: Symlink("../i/a", "fs/j/k"),
checks: CheckWithScope("k", "i/a", "fs/j"),
},
{
name: "SymlinkToRoot",
apply: Symlink("/", "foo"),
checks: Check("foo", ""),
},
{
name: "SymlinkSlashDotdot",
apply: Symlink("/../../", "foo"),
checks: Check("foo", ""),
},
{
name: "SymlinkDotdot",
apply: Symlink("../../", "foo"),
checks: Check("foo", ""),
},
{
name: "SymlinkRelativePath2",
apply: Symlink("baz/target", "bar/foo"),
checks: Check("bar/foo", "bar/baz/target"),
},
{
name: "SymlinkScopeLink",
apply: fstest.Apply(
Symlink("root2", "root"),
Symlink("../bar", "root2/foo"),
),
checks: CheckWithScope("foo", "bar", "root"),
},
{
name: "SymlinkSelf",
apply: fstest.Apply(
Symlink("foo", "root/foo"),
),
checks: ErrorWithScope("foo", "root", errTooManyLinks),
},
{
name: "SymlinkCircular",
apply: fstest.Apply(
Symlink("foo", "bar"),
Symlink("bar", "foo"),
),
checks: ErrorWithScope("foo", "", errTooManyLinks), //TODO: Test for circular error
},
{
name: "SymlinkCircularUnderRoot",
apply: fstest.Apply(
Symlink("baz", "root/bar"),
Symlink("../bak", "root/baz"),
Symlink("/bar", "root/bak"),
),
checks: ErrorWithScope("bar", "root", errTooManyLinks), // TODO: Test for circular error
},
{
name: "SymlinkComplexChain",
apply: fstest.Apply(
fstest.CreateDir("root2", 0777),
Symlink("root2", "root"),
Symlink("r/s", "root/a"),
Symlink("../root/t", "root/r"),
Symlink("/../u", "root/root/t/s/b"),
Symlink(".", "root/u/c"),
Symlink("../v", "root/u/x/y"),
Symlink("/../w", "root/u/v"),
),
checks: CheckWithScope("a/b/c/x/y/z", "w/z", "root"), // Original return
},
{
name: "SymlinkBreakoutNonExistent",
apply: fstest.Apply(
Symlink("/", "root/slash"),
Symlink("/idontexist/../slash", "root/sym"),
),
checks: CheckWithScope("sym/file", "file", "root"),
},
{
name: "SymlinkNoLexicalCleaning",
apply: fstest.Apply(
Symlink("/foo/bar", "root/sym"),
Symlink("/sym/../baz", "root/hello"),
),
checks: CheckWithScope("hello", "foo/baz", "root"),
},
}
for _, test := range tests {
t.Run(test.name, makeRootPathTest(t, test.apply, test.checks))
}
// Add related tests which are unable to follow same pattern
t.Run("SymlinkRootScope", testRootPathSymlinkRootScope)
t.Run("SymlinkEmpty", testRootPathSymlinkEmpty)
}
func testRootPathSymlinkRootScope(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "TestFollowSymlinkRootScope")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
expected, err := filepath.EvalSymlinks(tmpdir)
if err != nil {
t.Fatal(err)
}
rewrite, err := rootPath("/", tmpdir)
if err != nil {
t.Fatal(err)
}
if rewrite != expected {
t.Fatalf("expected %q got %q", expected, rewrite)
}
}
func testRootPathSymlinkEmpty(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
res, err := rootPath(wd, "")
if err != nil {
t.Fatal(err)
}
if res != wd {
t.Fatalf("expected %q got %q", wd, res)
}
}
func makeRootPathTest(t *testing.T, apply fstest.Applier, checks []rootCheck) func(t *testing.T) {
return func(t *testing.T) {
applyDir, err := ioutil.TempDir("", "test-root-path-")
if err != nil {
t.Fatalf("Unable to make temp directory: %+v", err)
}
defer os.RemoveAll(applyDir)
if apply != nil {
if err := apply.Apply(applyDir); err != nil {
t.Fatalf("Apply failed: %+v", err)
}
}
for i, check := range checks {
root := applyDir
if check.scope != nil {
root = check.scope(root)
}
actual, err := rootPath(root, check.unresolved)
if check.cause != nil {
if err == nil {
t.Errorf("(Check %d) Expected error %q, %q evaluated as %q", i+1, check.cause.Error(), check.unresolved, actual)
}
if errors.Cause(err) != check.cause {
t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
}
} else {
expected := filepath.Join(root, check.expected)
if err != nil {
t.Fatalf("(Check %d) Failed to evaluate root path: %+v", i+1, err)
}
if actual != expected {
t.Errorf("(Check %d) Unexpected evaluated path %q, expected %q", i+1, actual, expected)
}
}
}
}
}
func Check(unresolved, expected string) []rootCheck {
return []rootCheck{
{
unresolved: unresolved,
expected: expected,
},
}
}
func CheckWithScope(unresolved, expected, scope string) []rootCheck {
return []rootCheck{
{
unresolved: unresolved,
expected: expected,
scope: func(root string) string {
return filepath.Join(root, scope)
},
},
}
}
func ErrorWithScope(unresolved, scope string, cause error) []rootCheck {
return []rootCheck{
{
unresolved: unresolved,
cause: cause,
scope: func(root string) string {
return filepath.Join(root, scope)
},
},
}
}
func CheckAll(checks ...[]rootCheck) []rootCheck {
all := make([]rootCheck, 0, len(checks))
for _, c := range checks {
all = append(all, c...)
}
return all
}
func Symlink(oldname, newname string) fstest.Applier {
dir := filepath.Dir(newname)
if dir != "" {
return fstest.Apply(
fstest.CreateDir(dir, 0755),
fstest.Symlink(oldname, newname),
)
}
return fstest.Symlink(oldname, newname)
}