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