Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/shp_build_upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ In case a source bundle image is defined, the bundling feature is used, which wi
source code into a bundle container and upload it to the specified container registry. Instead of
executing using Git in the source step, it will use the container registry to obtain the source code.

$ shp buildrun upload <build-name>
$ shp buildrun upload <build-name> /path/to/repository
$ shp build upload <build-name>
$ shp build upload <build-name> /path/to/repository


```
Expand Down
6 changes: 3 additions & 3 deletions pkg/shp/cmd/build/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ In case a source bundle image is defined, the bundling feature is used, which wi
source code into a bundle container and upload it to the specified container registry. Instead of
executing using Git in the source step, it will use the container registry to obtain the source code.
$ shp buildrun upload <build-name>
$ shp buildrun upload <build-name> /path/to/repository
$ shp build upload <build-name>
$ shp build upload <build-name> /path/to/repository
`

// targetBaseDir directory where data will be uploaded.
Expand Down Expand Up @@ -211,7 +211,7 @@ func (u *UploadCommand) performDataStreaming(target *streamer.Target) error {

fmt.Fprintf(u.ioStreams.Out, "Streaming %q to the Build POD %q ...\n", u.sourceDir, target.Pod)
// creates an in-memory tarball with source directory data, and ready to start data streaming
tarball, err := streamer.NewTar(u.sourceDir)
tarball, err := streamer.NewTar(u.sourceDir, u.ioStreams)
if err != nil {
return err
}
Expand Down
14 changes: 8 additions & 6 deletions pkg/shp/streamer/tar.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import (
"strings"

ignore "github.com/sabhiram/go-gitignore"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

// Tar helper to create a tar instance based on a source directory, skipping entries that are not
// desired like `.git` directory and entries in `.gitignore` file.
type Tar struct {
src string // base directory
gitIgnore *ignore.GitIgnore // matcher for git ignored files
src string // base directory
gitIgnore *ignore.GitIgnore // matcher for git ignored files
ioStreams *genericclioptions.IOStreams // io streams for user-facing output
}

// skipPath inspect each path and makes sure it skips files the tar helper can't handle.
func (t *Tar) skipPath(fpath string, stat fs.FileInfo) bool {
if !stat.Mode().IsRegular() {
if !stat.Mode().IsRegular() && stat.Mode()&fs.ModeSymlink == 0 {
return true
}
if strings.HasPrefix(fpath, path.Join(t.src, ".git")) {
Expand All @@ -43,7 +45,7 @@ func (t *Tar) Create(w io.Writer) error {
if t.skipPath(fpath, stat) {
return nil
}
return writeFileToTar(tw, t.src, fpath, stat)
return writeFileToTar(tw, t, fpath, stat)
}); err != nil {
return err
}
Expand All @@ -63,8 +65,8 @@ func (t *Tar) bootstrap() error {
}

// NewTar instantiate a tar helper based on the source directory path informed.
func NewTar(src string) (*Tar, error) {
t := &Tar{src: src}
func NewTar(src string, ioStreams *genericclioptions.IOStreams) (*Tar, error) {
t := &Tar{src: src, ioStreams: ioStreams}
return t, t.bootstrap()
}

Expand Down
37 changes: 34 additions & 3 deletions pkg/shp/streamer/tar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,38 @@
import (
"archive/tar"
"io"
"os"
"strings"
"testing"

o "github.com/onsi/gomega"
"k8s.io/cli-runtime/pkg/genericclioptions"
)

func Test_Tar(t *testing.T) {
g := o.NewGomegaWithT(t)

tarHelper, err := NewTar("../../..")
ioStreams, _, _, errOut := genericclioptions.NewTestIOStreams()

baseDir := "../../.."

symlinks := []struct {
name string
target string
}{
{name: "symlink_inside_tree", target: "../test"},
{name: "symlink_outside_tree", target: "../../foobar/test"},
}

for _, symlink := range symlinks {
path := baseDir + "/test/" + symlink.name
if err := os.Symlink(symlink.target, path); err != nil {
t.Fatalf("Couldn't setup test symlink: %v", err)
}
t.Cleanup(func() { os.Remove(path) })

Check failure on line 34 in pkg/shp/streamer/tar_test.go

View workflow job for this annotation

GitHub Actions / verify

G104: Errors unhandled (gosec)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Golangci-lint failing here on an unhandled error from os.Remove. I'm fine capturing the error and logging it.

Suggested change
t.Cleanup(func() { os.Remove(path) })
t.Cleanup(func() {
cleanupErr := os.Remove(path)
if cleanupErr != nil {
t.Logf("failed to remove path %q: %v", path, err)
}
})

}

tarHelper, err := NewTar(baseDir, &ioStreams)
g.Expect(err).To(o.BeNil())

reader, writer := io.Pipe()
Expand All @@ -25,7 +47,7 @@
}()

tarReader := tar.NewReader(reader)
counter := 0
counter, symlink := 0, 0
for {
header, err := tarReader.Next()
if err != nil {
Expand All @@ -37,10 +59,19 @@
counter++
name := header.Name

// making sure that undesired entries are not present on the list of files caputured by the
if header.Linkname != "" {
symlink++
}

// making sure that undesired entries are not present on the list of files captured by the
// tar helper
g.Expect(strings.HasPrefix(name, ".git/")).To(o.BeFalse())
g.Expect(strings.HasPrefix(name, "_output/")).To(o.BeFalse())
}
g.Expect(counter > 10).To(o.BeTrue())

stdErr := errOut.String()

g.Expect(symlink > 1).To(o.BeTrue())
g.Expect(stdErr).To(o.ContainSubstring("points outside the source directory"))
}
69 changes: 61 additions & 8 deletions pkg/shp/streamer/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package streamer

import (
"archive/tar"
"fmt"
"io"
"io/fs"
"os"
Expand All @@ -21,24 +22,76 @@ func trimPrefix(prefix, fpath string) string {
return strings.TrimPrefix(strings.ReplaceAll(fpath, prefix, ""), string(filepath.Separator))
}

func writeFileToTar(tw *tar.Writer, src, fpath string, stat fs.FileInfo) error {
func writeFileToTar(tw *tar.Writer, t *Tar, fpath string, stat fs.FileInfo) error {
header, err := tar.FileInfoHeader(stat, stat.Name())
if err != nil {
return err
}

header.Name = trimPrefix(src, fpath)
header.Name = trimPrefix(t.src, fpath)

// Symlink
if stat.Mode()&fs.ModeSymlink != 0 {
target, err := os.Readlink(fpath)
if err != nil {
return err
}

// resolving target to absolute path, to determine if it is pointing
// outside the source directory.
var absSrc, absTarget string

if absSrc, err = filepath.Abs(t.src); err != nil {
return err
}

if filepath.IsAbs(target) {
absTarget = target
} else {
fullTarget := filepath.Join(filepath.Dir(fpath), target)
if absTarget, err = filepath.Abs(fullTarget); err != nil {
return err
}
}

if outside, _ := isSymlinkTargetOutsideOfDir(absSrc, absTarget); outside {
relPath, _ := filepath.Rel(t.src, fpath)
fmt.Fprintf(t.ioStreams.ErrOut, "WARNING: symlink %q points outside the source directory %q (target: %q)\n", relPath, absSrc, target)
}
Comment on lines +57 to +60
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fail here, not warn. Reading the contents of a symlink that is outside of a given "directory root" is a very common vulnerability. See CWE-61.


header.Linkname = target
Comment thread
dollierp marked this conversation as resolved.
}

if err := tw.WriteHeader(header); err != nil {
return err
}

// #nosec G304 intentionally opening file from variable
f, err := os.Open(fpath)
// Copy regular file content
if stat.Mode().IsRegular() {
// #nosec G304 intentionally opening file from variable
f, err := os.Open(fpath)
if err != nil {
return err
}
if _, err := io.Copy(tw, f); err != nil {
return err
}
return f.Close()
}

return nil
}

func isSymlinkTargetOutsideOfDir(dir, target string) (bool, error) {
Comment thread
dollierp marked this conversation as resolved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding a godoc here around CWE-61

Suggested change
func isSymlinkTargetOutsideOfDir(dir, target string) (bool, error) {
// isSymlinkTargetOutsideOfDir checks if the provided link target resolves to an absolute path outside
// of the provided directory. Symlinks to absolute paths outside of a given "root" directory is a common
// attack vector, as these can potentially bypass operating system permission checks that block access to
// sensitive data. See [CWE-61](https://cwe.mitre.org/data/definitions/61.html).
func isSymlinkTargetOutsideOfDir(dir, target string) (bool, error) {

realTarget, err := filepath.EvalSymlinks(target)
if err != nil {
return err
realTarget = filepath.Clean(target)
}
if _, err := io.Copy(tw, f); err != nil {
return err

rel, err := filepath.Rel(dir, realTarget)
if err != nil {
return false, err
}
return f.Close()

return !filepath.IsLocal(rel), nil
}
Loading