package archive // import "github.com/docker/docker/pkg/archive" import ( "archive/tar" "io" "os" "path/filepath" "reflect" "testing" "github.com/docker/docker/pkg/ioutils" ) func TestApplyLayerInvalidFilenames(t *testing.T) { for i, headers := range [][]*tar.Header{ { { Name: "../victim/dotdot", Typeflag: tar.TypeReg, Mode: 0644, }, }, { { // Note the leading slash Name: "/../victim/slash-dotdot", Typeflag: tar.TypeReg, Mode: 0644, }, }, } { if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidFilenames", headers); err != nil { t.Fatalf("i=%d. %v", i, err) } } } func TestApplyLayerInvalidHardlink(t *testing.T) { for i, headers := range [][]*tar.Header{ { // try reading victim/hello (../) { Name: "dotdot", Typeflag: tar.TypeLink, Linkname: "../victim/hello", Mode: 0644, }, }, { // try reading victim/hello (/../) { Name: "slash-dotdot", Typeflag: tar.TypeLink, // Note the leading slash Linkname: "/../victim/hello", Mode: 0644, }, }, { // try writing victim/file { Name: "loophole-victim", Typeflag: tar.TypeLink, Linkname: "../victim", Mode: 0755, }, { Name: "loophole-victim/file", Typeflag: tar.TypeReg, Mode: 0644, }, }, { // try reading victim/hello (hardlink, symlink) { Name: "loophole-victim", Typeflag: tar.TypeLink, Linkname: "../victim", Mode: 0755, }, { Name: "symlink", Typeflag: tar.TypeSymlink, Linkname: "loophole-victim/hello", Mode: 0644, }, }, { // Try reading victim/hello (hardlink, hardlink) { Name: "loophole-victim", Typeflag: tar.TypeLink, Linkname: "../victim", Mode: 0755, }, { Name: "hardlink", Typeflag: tar.TypeLink, Linkname: "loophole-victim/hello", Mode: 0644, }, }, { // Try removing victim directory (hardlink) { Name: "loophole-victim", Typeflag: tar.TypeLink, Linkname: "../victim", Mode: 0755, }, { Name: "loophole-victim", Typeflag: tar.TypeReg, Mode: 0644, }, }, } { if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidHardlink", headers); err != nil { t.Fatalf("i=%d. %v", i, err) } } } func TestApplyLayerInvalidSymlink(t *testing.T) { for i, headers := range [][]*tar.Header{ { // try reading victim/hello (../) { Name: "dotdot", Typeflag: tar.TypeSymlink, Linkname: "../victim/hello", Mode: 0644, }, }, { // try reading victim/hello (/../) { Name: "slash-dotdot", Typeflag: tar.TypeSymlink, // Note the leading slash Linkname: "/../victim/hello", Mode: 0644, }, }, { // try writing victim/file { Name: "loophole-victim", Typeflag: tar.TypeSymlink, Linkname: "../victim", Mode: 0755, }, { Name: "loophole-victim/file", Typeflag: tar.TypeReg, Mode: 0644, }, }, { // try reading victim/hello (symlink, symlink) { Name: "loophole-victim", Typeflag: tar.TypeSymlink, Linkname: "../victim", Mode: 0755, }, { Name: "symlink", Typeflag: tar.TypeSymlink, Linkname: "loophole-victim/hello", Mode: 0644, }, }, { // try reading victim/hello (symlink, hardlink) { Name: "loophole-victim", Typeflag: tar.TypeSymlink, Linkname: "../victim", Mode: 0755, }, { Name: "hardlink", Typeflag: tar.TypeLink, Linkname: "loophole-victim/hello", Mode: 0644, }, }, { // try removing victim directory (symlink) { Name: "loophole-victim", Typeflag: tar.TypeSymlink, Linkname: "../victim", Mode: 0755, }, { Name: "loophole-victim", Typeflag: tar.TypeReg, Mode: 0644, }, }, } { if err := testBreakout("applylayer", "docker-TestApplyLayerInvalidSymlink", headers); err != nil { t.Fatalf("i=%d. %v", i, err) } } } func TestApplyLayerWhiteouts(t *testing.T) { wd, err := os.MkdirTemp("", "graphdriver-test-whiteouts") if err != nil { return } defer os.RemoveAll(wd) base := []string{ ".baz", "bar/", "bar/bax", "bar/bay/", "baz", "foo/", "foo/.abc", "foo/.bcd/", "foo/.bcd/a", "foo/cde/", "foo/cde/def", "foo/cde/efg", "foo/fgh", "foobar", } type tcase struct { change, expected []string } tcases := []tcase{ { base, base, }, { []string{ ".bay", ".wh.baz", "foo/", "foo/.bce", "foo/.wh..wh..opq", "foo/cde/", "foo/cde/efg", }, []string{ ".bay", ".baz", "bar/", "bar/bax", "bar/bay/", "foo/", "foo/.bce", "foo/cde/", "foo/cde/efg", "foobar", }, }, { []string{ ".bay", ".wh..baz", ".wh.foobar", "foo/", "foo/.abc", "foo/.wh.cde", "bar/", }, []string{ ".bay", "bar/", "bar/bax", "bar/bay/", "foo/", "foo/.abc", "foo/.bce", }, }, { []string{ ".abc", ".wh..wh..opq", "foobar", }, []string{ ".abc", "foobar", }, }, } for i, tc := range tcases { l, err := makeTestLayer(tc.change) if err != nil { t.Fatal(err) } _, err = UnpackLayer(wd, l, nil) if err != nil { t.Fatal(err) } err = l.Close() if err != nil { t.Fatal(err) } paths, err := readDirContents(wd) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(tc.expected, paths) { t.Fatalf("invalid files for layer %d: expected %q, got %q", i, tc.expected, paths) } } } func makeTestLayer(paths []string) (rc io.ReadCloser, err error) { tmpDir, err := os.MkdirTemp("", "graphdriver-test-mklayer") if err != nil { return } defer func() { if err != nil { os.RemoveAll(tmpDir) } }() for _, p := range paths { // Source files are always in Unix format. But we use filepath on // creation to be platform agnostic. if p[len(p)-1] == '/' { if err = os.MkdirAll(filepath.Join(tmpDir, p), 0700); err != nil { return } } else { if err = os.WriteFile(filepath.Join(tmpDir, p), nil, 0600); err != nil { return } } } archive, err := Tar(tmpDir, Uncompressed) if err != nil { return } return ioutils.NewReadCloserWrapper(archive, func() error { err := archive.Close() os.RemoveAll(tmpDir) return err }), nil } func readDirContents(root string) ([]string, error) { var files []string err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if path == root { return nil } rel, err := filepath.Rel(root, path) if err != nil { return err } if info.IsDir() { rel = rel + string(filepath.Separator) } // Append in Unix semantics files = append(files, filepath.ToSlash(rel)) return nil }) if err != nil { return nil, err } return files, nil }