Skip to content

Commit

Permalink
Merge pull request #655 from fluxcd/oci-skip-symlinks
Browse files Browse the repository at this point in the history
Skip symlinks during OCI layer decompression
  • Loading branch information
stefanprodan authored Sep 29, 2023
2 parents 4f69e78 + c62dc82 commit 57d9588
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 12 deletions.
2 changes: 1 addition & 1 deletion http/fetch/archive_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (r *ArchiveFetcher) Fetch(archiveURL, digest, dir string) error {
}

// Extracts the tar file.
if err = tar.Untar(f, dir, tar.WithMaxUntarSize(r.maxUntarSize)); err != nil {
if err = tar.Untar(f, dir, tar.WithMaxUntarSize(r.maxUntarSize), tar.WithSkipSymlinks()); err != nil {
return fmt.Errorf("failed to extract archive (check whether file size exceeds max download size): %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion http/fetch/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ require (
)

require (
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/zeebo/blake3 v0.1.1 // indirect
Expand Down
4 changes: 2 additions & 2 deletions http/fetch/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
Expand Down
5 changes: 3 additions & 2 deletions oci/client/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import (
"context"
"fmt"

"github.com/fluxcd/pkg/tar"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/google/go-containerregistry/pkg/name"

"github.com/fluxcd/pkg/tar"
)

// Pull downloads an artifact from an OCI repository and extracts the content to the given directory.
Expand Down Expand Up @@ -65,7 +66,7 @@ func (c *Client) Pull(ctx context.Context, url, outDir string) (*Metadata, error
return nil, fmt.Errorf("extracting first layer failed: %w", err)
}

if err = tar.Untar(blob, outDir, tar.WithMaxUntarSize(-1)); err != nil {
if err = tar.Untar(blob, outDir, tar.WithMaxUntarSize(-1), tar.WithSkipSymlinks()); err != nil {
return nil, fmt.Errorf("failed to untar first layer: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion oci/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ require (
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v24.0.0+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
Expand Down
4 changes: 2 additions & 2 deletions oci/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSk
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
2 changes: 1 addition & 1 deletion tar/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/fluxcd/pkg/tar

go 1.20

require github.com/cyphar/filepath-securejoin v0.2.3
require github.com/cyphar/filepath-securejoin v0.2.4
4 changes: 2 additions & 2 deletions tar/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
128 changes: 128 additions & 0 deletions tar/symlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package tar

import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"os"
"path"
"path/filepath"
"testing"
)

func TestSkipSymlinks(t *testing.T) {
tmpDir := t.TempDir()

symlinkTarget := filepath.Join(tmpDir, "symlink.target")
err := os.WriteFile(symlinkTarget, geRandomContent(256), os.ModePerm)
if err != nil {
t.Fatal(err)
}

symlink := filepath.Join(tmpDir, "symlink")
err = os.Symlink(symlinkTarget, symlink)
if err != nil {
t.Fatal(err)
}

tgzFileName := filepath.Join(t.TempDir(), "test.tgz")
var buf bytes.Buffer
err = tgzWithSymlinks(tmpDir, &buf)
if err != nil {
t.Fatal(err)
}

tgzFile, err := os.OpenFile(tgzFileName, os.O_CREATE|os.O_RDWR, os.ModePerm)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(tgzFile, &buf); err != nil {
t.Fatal(err)
}
if err = tgzFile.Close(); err != nil {
t.Fatal(err)
}

targetDirOutput := filepath.Join(t.TempDir(), "output")
f1, err := os.Open(tgzFileName)
if err != nil {
t.Fatal(err)
}

err = Untar(f1, targetDirOutput, WithMaxUntarSize(-1))
if err == nil {
t.Errorf("wanted error: unsupported symlink")
}

f2, err := os.Open(tgzFileName)
if err != nil {
t.Fatal(err)
}

err = Untar(f2, targetDirOutput, WithMaxUntarSize(-1), WithSkipSymlinks())
if err != nil {
t.Errorf("unexpected error: %v", err)
}

if _, err := os.Open(path.Join(targetDirOutput, "symlink.target")); err != nil {
t.Errorf("regular file not found: %v", err)
}
}

func tgzWithSymlinks(src string, buf io.Writer) error {
absDir, err := filepath.Abs(src)
if err != nil {
return err
}

zr := gzip.NewWriter(buf)
tw := tar.NewWriter(zr)
if err := filepath.Walk(absDir, func(file string, fi os.FileInfo, err error) error {
header, err := tar.FileInfoHeader(fi, file)
if err != nil {
return err
}
if err := tw.WriteHeader(header); err != nil {
return err
}

if fi.Mode().IsRegular() {
f, err := os.Open(file)
if err != nil {
return err
}
if _, err := io.Copy(tw, f); err != nil {
return err
}
return f.Close()
}

return nil
}); err != nil {
return err
}
if err := tw.Close(); err != nil {
return err
}
if err := zr.Close(); err != nil {
return err
}
return nil
}
7 changes: 7 additions & 0 deletions tar/tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type tarOpts struct {
// maxUntarSize represents the limit size (bytes) for archives being decompressed by Untar.
// When max is a negative value the size checks are disabled.
maxUntarSize int

// skipSymlinks ignores symlinks instead of failing the decompression.
skipSymlinks bool
}

// Untar reads the gzip-compressed tar file from r and writes it into dir.
Expand Down Expand Up @@ -168,6 +171,10 @@ func Untar(r io.Reader, dir string, inOpts ...TarOption) (err error) {
return err
}
madeDir[abs] = true
case mode&os.ModeSymlink == os.ModeSymlink:
if !opts.skipSymlinks {
return fmt.Errorf("tar file entry %s is a symlink, which is not allowed in this context", f.Name)
}
default:
return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
}
Expand Down
7 changes: 7 additions & 0 deletions tar/tar_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ func WithMaxUntarSize(max int) TarOption {
}
}

// WithSkipSymlinks allows for symlinks to be present in the tarball and skips them when decompressing.
func WithSkipSymlinks() TarOption {
return func(t *tarOpts) {
t.skipSymlinks = true
}
}

func (t *tarOpts) applyOpts(tarOpts ...TarOption) {
for _, clientOpt := range tarOpts {
clientOpt(t)
Expand Down

0 comments on commit 57d9588

Please sign in to comment.