New Upstream Release - golang-github-hashicorp-go-slug

Ready changes

Summary

Merged new upstream version: 0.10.1 (was: 0.9.1).

Resulting package

Built on 2023-03-29T22:44 (took 5m18s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-hashicorp-go-slug-dev

Lintian Result

Diff

diff --git a/.circleci/config.yml b/.circleci/config.yml
deleted file mode 100644
index 33b0318..0000000
--- a/.circleci/config.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-version: 2
-
-workflows:
-  version: 2
-  build:
-    jobs:
-      - test
-      
-jobs:
-  test:
-    docker:
-      - image: docker.mirror.hashicorp.services/cimg/go:1.15
-
-    steps:
-      - checkout
-      - run: go test -race ./...
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..9842b56
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,17 @@
+---
+name: test
+on: [push]
+
+jobs:
+  unit-test:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: setup go
+        uses: actions/setup-go@v3
+        with:
+          go-version-file: go.mod
+
+      - name: test
+        run: go test -race ./...
diff --git a/README.md b/README.md
index 67425c5..accfb58 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # go-slug
 
-[![Build Status](https://circleci.com/gh/hashicorp/go-slug.svg?style=shield)](https://app.circleci.com/pipelines/github/hashicorp/go-slug)
+[![Build Status](https://github.com/hashicorp/go-slug/actions/workflows/test.yml/badge.svg)](https://github.com/hashicorp/go-slug/actions/workflows/test.yml)
 [![GitHub license](https://img.shields.io/github/license/hashicorp/go-slug.svg)](https://github.com/hashicorp/go-slug/blob/master/LICENSE)
 [![GoDoc](https://godoc.org/github.com/hashicorp/go-slug?status.svg)](https://godoc.org/github.com/hashicorp/go-slug)
 [![Go Report Card](https://goreportcard.com/badge/github.com/hashicorp/go-slug)](https://goreportcard.com/report/github.com/hashicorp/go-slug)
diff --git a/debian/changelog b/debian/changelog
index dd4c8e3..53e864b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-hashicorp-go-slug (0.10.1-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Wed, 29 Mar 2023 22:39:42 -0000
+
 golang-github-hashicorp-go-slug (0.9.1-2) unstable; urgency=medium
 
   * reintroduce patch (Closes: #1030575)
diff --git a/debian/patches/remove-tests-with-testdata.patch b/debian/patches/remove-tests-with-testdata.patch
index 098121f..2205c98 100644
--- a/debian/patches/remove-tests-with-testdata.patch
+++ b/debian/patches/remove-tests-with-testdata.patch
@@ -1,8 +1,8 @@
-Index: golang-github-hashicorp-go-slug/slug_test.go
+Index: golang-github-hashicorp-go-slug.git/slug_test.go
 ===================================================================
---- golang-github-hashicorp-go-slug.orig/slug_test.go	2023-02-05 11:43:28.699878054 +0100
-+++ golang-github-hashicorp-go-slug/slug_test.go	2023-02-05 11:46:12.815921418 +0100
-@@ -15,203 +15,6 @@
+--- golang-github-hashicorp-go-slug.git.orig/slug_test.go
++++ golang-github-hashicorp-go-slug.git/slug_test.go
+@@ -15,203 +15,6 @@ import (
  	"testing"
  )
  
@@ -206,7 +206,7 @@ Index: golang-github-hashicorp-go-slug/slug_test.go
  func TestPack_symlinks(t *testing.T) {
  	type tcase struct {
  		absolute     bool
-@@ -361,38 +164,6 @@
+@@ -564,38 +367,6 @@ func TestAllowSymlinkTarget(t *testing.T
  	}
  }
  
@@ -245,11 +245,11 @@ Index: golang-github-hashicorp-go-slug/slug_test.go
  func TestUnpackDuplicateNoWritePerm(t *testing.T) {
  	dir, err := ioutil.TempDir("", "slug")
  	if err != nil {
-Index: golang-github-hashicorp-go-slug/terraformignore_test.go
+Index: golang-github-hashicorp-go-slug.git/terraformignore_test.go
 ===================================================================
---- golang-github-hashicorp-go-slug.orig/terraformignore_test.go	2023-02-05 11:43:28.699878054 +0100
-+++ golang-github-hashicorp-go-slug/terraformignore_test.go	2023-02-05 11:46:33.927926898 +0100
-@@ -11,104 +11,4 @@
+--- golang-github-hashicorp-go-slug.git.orig/terraformignore_test.go
++++ golang-github-hashicorp-go-slug.git/terraformignore_test.go
+@@ -11,104 +11,4 @@ func TestTerraformIgnore(t *testing.T) {
  		t.Fatal("A directory without .terraformignore should get the default patterns")
  	}
  
diff --git a/slug.go b/slug.go
index 365b5c4..86ea281 100644
--- a/slug.go
+++ b/slug.go
@@ -47,7 +47,8 @@ func ApplyTerraformIgnore() PackerOption {
 }
 
 // DereferenceSymlinks is a PackerOption that will allow symlinks that
-// reference a target outside of the src directory.
+// reference a target outside of the source directory by copying the link
+// target, turning it into a normal file within the archive.
 func DereferenceSymlinks() PackerOption {
 	return func(p *Packer) error {
 		p.dereference = true
@@ -55,10 +56,25 @@ func DereferenceSymlinks() PackerOption {
 	}
 }
 
+// AllowSymlinkTarget relaxes safety checks on symlinks with targets matching
+// path. Specifically, absolute symlink targets (e.g. "/foo/bar") and relative
+// targets (e.g. "../foo/bar") which resolve to a path outside of the
+// source/destination directories for pack/unpack operations respectively, may
+// be expressly permitted, whereas they are forbidden by default. Exercise
+// caution when using this option. A symlink matches path if its target
+// resolves to path exactly, or if path is a parent directory of target.
+func AllowSymlinkTarget(path string) PackerOption {
+	return func(p *Packer) error {
+		p.allowSymlinkTargets = append(p.allowSymlinkTargets, path)
+		return nil
+	}
+}
+
 // Packer holds options for the Pack function.
 type Packer struct {
 	dereference          bool
 	applyTerraformIgnore bool
+	allowSymlinkTargets  []string
 }
 
 // NewPacker is a constructor for Packer.
@@ -100,7 +116,12 @@ func Pack(src string, w io.Writer, dereference bool) (*Meta, error) {
 // from the slug.
 func (p *Packer) Pack(src string, w io.Writer) (*Meta, error) {
 	// Gzip compress all the output data.
-	gzipW := gzip.NewWriter(w)
+	gzipW, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
+	if err != nil {
+		// This error is only raised when an incorrect gzip level is
+		// specified.
+		return nil, err
+	}
 
 	// Tar the file contents.
 	tarW := tar.NewWriter(gzipW)
@@ -116,7 +137,7 @@ func (p *Packer) Pack(src string, w io.Writer) (*Meta, error) {
 	meta := &Meta{}
 
 	// Walk the tree of files.
-	err := filepath.Walk(src, packWalkFn(src, src, src, tarW, meta, p.dereference, ignoreRules))
+	err = filepath.Walk(src, p.packWalkFn(src, src, src, tarW, meta, ignoreRules))
 	if err != nil {
 		return nil, err
 	}
@@ -134,7 +155,7 @@ func (p *Packer) Pack(src string, w io.Writer) (*Meta, error) {
 	return meta, nil
 }
 
-func packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, dereference bool, ignoreRules []rule) filepath.WalkFunc {
+func (p *Packer) packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, ignoreRules []rule) filepath.WalkFunc {
 	return func(path string, info os.FileInfo, err error) error {
 		if err != nil {
 			return err
@@ -196,55 +217,33 @@ func packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, dereference
 			// First read the symlink file to find the destination.
 			target, err := os.Readlink(path)
 			if err != nil {
-				return fmt.Errorf("failed to get symbolic link destination for %q: %w", path, err)
-			}
-
-			// Try to make absolute paths relative.
-			if filepath.IsAbs(target) {
-				absPath, err := filepath.Abs(path)
-				if err != nil {
-					return fmt.Errorf("failed to get absolute path for %q: %w", path, err)
-				}
-				absDir := filepath.Dir(absPath)
-
-				rel, err := filepath.Rel(absDir, target)
-				if err != nil {
-					return fmt.Errorf("failed to find relative path for %q: %w", target, err)
-				}
-				target = rel
+				return fmt.Errorf("failed to read symlink %q: %w", path, err)
 			}
 
-			// Get the path to the target relative to path.
-			target = filepath.Join(filepath.Dir(path), target)
-
-			// If the target is within the current source, we
-			// create the symlink using a relative path.
-			if strings.HasPrefix(target, src) {
-				link, err := filepath.Rel(filepath.Dir(path), target)
-				if err != nil {
-					return fmt.Errorf("failed to get relative path for symlink destination %q: %w", target, err)
+			// Ensure the target is acceptable per the Packer's configuration.
+			if err := p.checkSymlink(root, path, target); err != nil {
+				// Check if dereferencing is enabled. If so, we're going to
+				// try copying the symlink's data. If not, this is an error.
+				if !p.dereference {
+					return err
 				}
-
+			} else {
 				header.Typeflag = tar.TypeSymlink
-				header.Linkname = filepath.ToSlash(link)
-
-				// Break out of the case as a symlink
-				// doesn't need any additional config.
+				header.Linkname = filepath.ToSlash(target)
 				break
 			}
 
-			if !dereference {
-				// Symlinks with targets outside of src are prohibited.
-				return &IllegalSlugError{
-					Err: fmt.Errorf(
-						"invalid symlink (%q -> %q) has target outside of %q",
-						path, target, src,
-					),
-				}
+			// Get the absolute path of the symlink target.
+			absTarget := target
+			if !filepath.IsAbs(absTarget) {
+				absTarget = filepath.Join(filepath.Dir(path), target)
+			}
+			if !filepath.IsAbs(absTarget) {
+				absTarget = filepath.Join(root, absTarget)
 			}
 
 			// Get the file info for the target.
-			info, err = os.Lstat(target)
+			info, err = os.Lstat(absTarget)
 			if err != nil {
 				return fmt.Errorf("failed to get file info from file %q: %w", target, err)
 			}
@@ -252,7 +251,7 @@ func packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, dereference
 			// If the target is a directory we can recurse into the target
 			// directory by calling the packWalkFn with updated arguments.
 			if info.IsDir() {
-				return filepath.Walk(target, packWalkFn(root, target, path, tarW, meta, dereference, ignoreRules))
+				return filepath.Walk(absTarget, p.packWalkFn(root, target, path, tarW, meta, ignoreRules))
 			}
 
 			// Dereference this symlink by updating the header with the target file
@@ -302,6 +301,12 @@ func packWalkFn(root, src, dst string, tarW *tar.Writer, meta *Meta, dereference
 // directory. Symlinks within the slug are supported, provided their targets
 // are relative and point to paths within the destination directory.
 func Unpack(r io.Reader, dst string) error {
+	p := &Packer{}
+	return p.Unpack(r, dst)
+}
+
+// Unpack unpacks the archive data in r into directory dst.
+func (p *Packer) Unpack(r io.Reader, dst string) error {
 	// Decompress as we read.
 	uncompressed, err := gzip.NewReader(r)
 	if err != nil {
@@ -377,26 +382,9 @@ func Unpack(r io.Reader, dst string) error {
 
 		// Handle symlinks.
 		if header.Typeflag == tar.TypeSymlink {
-			// Disallow absolute targets.
-			if filepath.IsAbs(header.Linkname) {
-				return &IllegalSlugError{
-					Err: fmt.Errorf(
-						"invalid symlink (%q -> %q) has absolute target",
-						header.Name, header.Linkname,
-					),
-				}
-			}
-
-			// Ensure the link target is within the destination directory. This
-			// disallows providing symlinks to external files and directories.
-			target := filepath.Join(dir, header.Linkname)
-			if !strings.HasPrefix(target, dst) {
-				return &IllegalSlugError{
-					Err: fmt.Errorf(
-						"invalid symlink (%q -> %q) has external target",
-						header.Name, header.Linkname,
-					),
-				}
+			err := p.checkSymlink(dst, header.Name, header.Linkname)
+			if err != nil {
+				return err
 			}
 
 			// Create the symlink.
@@ -449,6 +437,64 @@ func Unpack(r io.Reader, dst string) error {
 	return nil
 }
 
+// Given a "root" directory, the path to a symlink within said root, and the
+// target of said symlink, checkSymlink checks that the target either falls
+// into root somewhere, or is explicitly allowed per the Packer's config.
+func (p *Packer) checkSymlink(root, path, target string) error {
+	// Get the absolute path to root.
+	absRoot, err := filepath.Abs(root)
+	if err != nil {
+		return fmt.Errorf("failed making path %q absolute: %w", root, err)
+	}
+
+	// Get the absolute path to the file path.
+	absPath := path
+	if !filepath.IsAbs(absPath) {
+		absPath = filepath.Join(absRoot, path)
+	}
+
+	// Get the absolute path of the symlink target.
+	var absTarget string
+	if filepath.IsAbs(target) {
+		absTarget = filepath.Clean(target)
+	} else {
+		absTarget = filepath.Join(filepath.Dir(absPath), target)
+	}
+
+	// Target falls within root.
+	if strings.HasPrefix(absTarget, absRoot) {
+		return nil
+	}
+
+	// The link target is outside of root. Check if it is allowed.
+	for _, prefix := range p.allowSymlinkTargets {
+		// Ensure prefix is absolute.
+		if !filepath.IsAbs(prefix) {
+			prefix = filepath.Join(absRoot, prefix)
+		}
+
+		// Exact match is allowed.
+		if absTarget == prefix {
+			return nil
+		}
+
+		// Prefix match of a directory is allowed.
+		if !strings.HasSuffix(prefix, "/") {
+			prefix += "/"
+		}
+		if strings.HasPrefix(absTarget, prefix) {
+			return nil
+		}
+	}
+
+	return &IllegalSlugError{
+		Err: fmt.Errorf(
+			"invalid symlink (%q -> %q) has external target",
+			path, target,
+		),
+	}
+}
+
 // checkFileMode is used to examine an os.FileMode and determine if it should
 // be included in the archive, and if it has a data body which needs writing.
 func checkFileMode(m os.FileMode) (keep, body bool) {
diff --git a/slug_test.go b/slug_test.go
index e0e1522..6639414 100644
--- a/slug_test.go
+++ b/slug_test.go
@@ -299,7 +299,7 @@ func TestPack_symlinks(t *testing.T) {
 
 			var expectErr string
 			if tc.external && !tc.dereference {
-				expectErr = "target outside"
+				expectErr = "has external target"
 			}
 			if tc.external && tc.dereference && !tc.targetExists {
 				expectErr = "no such file or directory"
@@ -348,7 +348,7 @@ func TestPack_symlinks(t *testing.T) {
 					if hdr.Typeflag != expectTypeflag {
 						t.Fatalf("unexpected file type in slug: %q", hdr.Typeflag)
 					}
-					if expectTypeflag == tar.TypeSymlink && hdr.Linkname != "../foo/bar" {
+					if expectTypeflag == tar.TypeSymlink && hdr.Linkname != targetPath {
 						t.Fatalf("unexpected link target in slug: %q", hdr.Linkname)
 					}
 				}
@@ -361,6 +361,209 @@ func TestPack_symlinks(t *testing.T) {
 	}
 }
 
+func TestAllowSymlinkTarget(t *testing.T) {
+	tcases := []struct {
+		desc   string
+		allow  string
+		target string
+		err    string
+	}{
+		{
+			desc:   "absolute symlink, exact match",
+			allow:  "/foo/bar/baz",
+			target: "/foo/bar/baz",
+		},
+		{
+			desc:   "relative symlink, exact match",
+			allow:  "../foo/bar",
+			target: "../foo/bar",
+		},
+		{
+			desc:   "absolute symlink, prefix match",
+			allow:  "/foo/",
+			target: "/foo/bar/baz",
+		},
+		{
+			desc:   "relative symlink, prefix match",
+			allow:  "../foo/",
+			target: "../foo/bar/baz",
+		},
+		{
+			desc:   "absolute symlink, non-match",
+			allow:  "/zip",
+			target: "/foo/bar/baz",
+			err:    "has external target",
+		},
+		{
+			desc:   "relative symlink, non-match",
+			allow:  "../zip/",
+			target: "../foo/bar/baz",
+			err:    "has external target",
+		},
+		{
+			desc:   "absolute symlink, embedded traversal, non-match",
+			allow:  "/foo/",
+			target: "/foo/../../zip",
+			err:    "has external target",
+		},
+		{
+			desc:   "relative symlink, embedded traversal, non-match",
+			allow:  "../foo/",
+			target: "../foo/../../zip",
+			err:    "has external target",
+		},
+		{
+			desc:   "absolute symlink, embedded traversal, match",
+			allow:  "/foo/",
+			target: "/foo/bar/../baz",
+		},
+		{
+			desc:   "relative symlink, embedded traversal, match",
+			allow:  "../foo/",
+			target: "../foo/bar/../baz",
+		},
+		{
+			desc:   "external target with embedded upward path traversal",
+			allow:  "foo/bar/",
+			target: "foo/bar/../../../lol",
+			err:    "has external target",
+		},
+		{
+			desc:   "similar file path, non-match",
+			allow:  "/foo",
+			target: "/foobar",
+			err:    "has external target",
+		},
+	}
+
+	for _, tc := range tcases {
+		t.Run("Pack: "+tc.desc, func(t *testing.T) {
+			td, err := ioutil.TempDir("", "go-slug")
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer os.RemoveAll(td)
+
+			// Make the symlink.
+			if err := os.Symlink(tc.target, filepath.Join(td, "sym")); err != nil {
+				t.Fatal(err)
+			}
+
+			// Pack up the temp dir.
+			slug := bytes.NewBuffer(nil)
+			p, err := NewPacker(AllowSymlinkTarget(tc.allow))
+			if err != nil {
+				t.Fatal(err)
+			}
+			_, err = p.Pack(td, slug)
+			if tc.err != "" {
+				if err != nil {
+					if strings.Contains(err.Error(), tc.err) {
+						return
+					}
+					t.Fatalf("expected error %q, got %v", tc.err, err)
+				}
+				t.Fatal("expected error, got nil")
+			} else if err != nil {
+				t.Fatal(err)
+			}
+
+			// Inspect the result.
+			gzipR, err := gzip.NewReader(slug)
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+			tarR := tar.NewReader(gzipR)
+
+			symFound := false
+			for {
+				hdr, err := tarR.Next()
+				if err == io.EOF {
+					break
+				}
+				if err != nil {
+					t.Fatalf("err: %v", err)
+				}
+				if hdr.Name == "sym" {
+					symFound = true
+					if hdr.Typeflag != tar.TypeSymlink {
+						t.Fatalf("unexpected file type in slug: %q", hdr.Typeflag)
+					}
+					if hdr.Linkname != tc.target {
+						t.Fatalf("unexpected link target in slug: %q", hdr.Linkname)
+					}
+				}
+			}
+
+			if !symFound {
+				t.Fatal("did not find symlink in archive")
+			}
+		})
+
+		t.Run("Unpack: "+tc.desc, func(t *testing.T) {
+			dir, err := ioutil.TempDir("", "slug")
+			if err != nil {
+				t.Fatalf("err:%v", err)
+			}
+			defer os.RemoveAll(dir)
+			in := filepath.Join(dir, "slug.tar.gz")
+
+			// Create the output file
+			wfh, err := os.Create(in)
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+
+			// Gzip compress all the output data
+			gzipW := gzip.NewWriter(wfh)
+
+			// Tar the file contents
+			tarW := tar.NewWriter(gzipW)
+
+			// Write the header.
+			tarW.WriteHeader(&tar.Header{
+				Name:     "l",
+				Linkname: tc.target,
+				Typeflag: tar.TypeSymlink,
+			})
+
+			tarW.Close()
+			gzipW.Close()
+			wfh.Close()
+
+			// Open the slug file for reading.
+			fh, err := os.Open(in)
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+
+			// Create a dir to unpack into.
+			dst, err := ioutil.TempDir(dir, "")
+			if err != nil {
+				t.Fatalf("err: %v", err)
+			}
+			defer os.RemoveAll(dst)
+
+			// Unpack.
+			p, err := NewPacker(AllowSymlinkTarget(tc.allow))
+			if err != nil {
+				t.Fatal(err)
+			}
+			if err := p.Unpack(fh, dst); err != nil {
+				if tc.err != "" {
+					if !strings.Contains(err.Error(), tc.err) {
+						t.Fatalf("expected error %q, got %v", tc.err, err)
+					}
+				} else {
+					t.Fatal(err)
+				}
+			} else if tc.err != "" {
+				t.Fatalf("expected error %q, got nil", tc.err)
+			}
+		})
+	}
+}
+
 func TestUnpack(t *testing.T) {
 	// First create the slug file so we can try to unpack it.
 	slug := bytes.NewBuffer(nil)
@@ -608,7 +811,7 @@ func TestUnpackMaliciousSymlinks(t *testing.T) {
 					Typeflag: tar.TypeSymlink,
 				},
 			},
-			err: "has absolute target",
+			err: "has external target",
 		},
 		{
 			desc: "symlink with external target",

Debdiff

File lists identical (after any substitutions)

No differences were encountered in the control files

More details

Full run details