diff --git a/Dockerfile.utils b/Dockerfile.utils index bb5e8c408b..3bf0aed4b7 100644 --- a/Dockerfile.utils +++ b/Dockerfile.utils @@ -34,6 +34,7 @@ COPY --from=builder /workspace/func-util /usr/local/bin/ RUN ln -s /usr/local/bin/func-util /usr/local/bin/deploy && \ ln -s /usr/local/bin/func-util /usr/local/bin/scaffold && \ ln -s /usr/local/bin/func-util /usr/local/bin/s2i && \ + ln -s /usr/local/bin/func-util /usr/local/bin/sh && \ ln -s /usr/local/bin/func-util /usr/local/bin/socat LABEL \ diff --git a/cmd/func-util/main.go b/cmd/func-util/main.go index 39f355c0b3..f1e4941109 100644 --- a/cmd/func-util/main.go +++ b/cmd/func-util/main.go @@ -10,8 +10,11 @@ import ( "os" "os/signal" "path/filepath" + "slices" "syscall" + "golang.org/x/sys/unix" + "github.com/openshift/source-to-image/pkg/cmd/cli" "k8s.io/klog/v2" @@ -20,6 +23,7 @@ import ( "knative.dev/func/pkg/k8s" "knative.dev/func/pkg/knative" "knative.dev/func/pkg/scaffolding" + "knative.dev/func/pkg/tar" ) func main() { @@ -46,6 +50,10 @@ func main() { cmd = s2iCmd case "socat": cmd = socat + case "sh": + cmd = sh + default: + cmd = sh } err := cmd(ctx) @@ -167,3 +175,18 @@ func (d deployDecorator) UpdateLabels(function fn.Function, labels map[string]st } return labels } + +func sh(ctx context.Context) error { + if !slices.Equal(os.Args[1:], []string{"-c", "umask 0000 && exec tar -xmf -"}) { + return fmt.Errorf("this is a fake sh (only for backward compatiblility purposes)") + } + + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("cannot get working directory: %w", err) + } + + unix.Umask(0) + + return tar.Extract(os.Stdin, wd) +} diff --git a/go.mod b/go.mod index 6a45569931..bc27979750 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( golang.org/x/net v0.34.0 golang.org/x/oauth2 v0.24.0 golang.org/x/sync v0.10.0 + golang.org/x/sys v0.29.0 golang.org/x/term v0.28.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -272,7 +273,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/pkg/k8s/testdata/content.tar b/pkg/k8s/testdata/content.tar index 0157ebaa12..30dd9c82f1 100644 Binary files a/pkg/k8s/testdata/content.tar and b/pkg/k8s/testdata/content.tar differ diff --git a/pkg/tar/tar.go b/pkg/tar/tar.go new file mode 100644 index 0000000000..d5d170111f --- /dev/null +++ b/pkg/tar/tar.go @@ -0,0 +1,89 @@ +package tar + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "strings" +) + +func Extract(input io.Reader, targetDir string) error { + var err error + + r := tar.NewReader(input) + + var first bool = true + for { + var hdr *tar.Header + hdr, err = r.Next() + if err != nil { + if errors.Is(err, io.EOF) { + if first { + // mimic tar output on empty input + return fmt.Errorf("does not look like a tar") + } + return nil + } + return err + } + + if strings.Contains(hdr.Name, "..") { + return fmt.Errorf("name contains '..': %s", hdr.Name) + } + if path.IsAbs(hdr.Linkname) { + return fmt.Errorf("absolute symlink: %s->%s", hdr.Name, hdr.Linkname) + } + if strings.HasPrefix(path.Clean(path.Join(path.Dir(hdr.Name), hdr.Linkname)), "..") { + return fmt.Errorf("link target escapes: %s->%s", hdr.Name, hdr.Linkname) + } + + targetPath := filepath.Join(targetDir, filepath.FromSlash(hdr.Name)) + + // remove if already exists + err = os.Remove(targetPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("cannot remove: %w", err) + } + + // ensure parent + err = os.MkdirAll(filepath.Dir(targetPath), os.FileMode(hdr.Mode)&fs.ModePerm|0111) + if err != nil { + return fmt.Errorf("cannot ensure parent: %w", err) + } + + first = false + switch { + case hdr.Typeflag == tar.TypeReg: + err = writeRegularFile(targetPath, os.FileMode(hdr.Mode&0777), r) + case hdr.Typeflag == tar.TypeDir: + err = os.MkdirAll(targetPath, os.FileMode(hdr.Mode)&fs.ModePerm) + case hdr.Typeflag == tar.TypeSymlink: + err = os.Symlink(hdr.Linkname, targetPath) + default: + _, _ = fmt.Printf("unsupported type flag: %d\n", hdr.Typeflag) + } + if err != nil { + return fmt.Errorf("cannot create entry: %w", err) + } + } +} + +func writeRegularFile(target string, perm os.FileMode, content io.Reader) error { + f, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) + if err != nil { + return err + } + defer func(f *os.File) { + _ = f.Close() + }(f) + _, err = io.Copy(f, content) + if err != nil { + return err + } + return nil +} diff --git a/pkg/tar/tar_errors_test.go b/pkg/tar/tar_errors_test.go new file mode 100644 index 0000000000..dfcd103d10 --- /dev/null +++ b/pkg/tar/tar_errors_test.go @@ -0,0 +1,420 @@ +package tar_test + +import ( + "archive/tar" + "bytes" + "io" + "os" + "testing" + + tarutil "knative.dev/func/pkg/tar" +) + +func TestExtract(t *testing.T) { + tests := []struct { + name string + createReader func(*testing.T) io.Reader + wantErr bool + }{ + { + name: "non escaping link", + createReader: nonEscapingSymlink, + wantErr: false, + }, + { + name: "missing parent of regular file", + createReader: missingParentRegular, + wantErr: false, + }, + { + name: "missing parent of link", + createReader: missingParentLink, + wantErr: false, + }, + { + name: "override regular file", + createReader: overrideRegularFile, + wantErr: false, + }, + { + name: "override link", + createReader: overrideLink, + wantErr: false, + }, + { + name: "absolute symlink", + createReader: absoluteSymlink, + wantErr: true, + }, + { + name: "direct escape", + createReader: directEscape, + wantErr: true, + }, + { + name: "indirect link escape", + createReader: indirectLinkEscape, + wantErr: true, + }, + { + name: "indirect link escape with overwrite", + createReader: indirectLinkEscapeWithOverwrites, + wantErr: true, + }, + { + name: "double dot in name", + createReader: doubleDotInName, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := t.TempDir() + if err := tarutil.Extract(tt.createReader(t), d); (err != nil) != tt.wantErr { + t.Errorf("Extract() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func nonEscapingSymlink(t *testing.T) io.Reader { + t.Helper() + + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "subdir", + Typeflag: tar.TypeDir, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + err = w.WriteHeader(&tar.Header{ + Name: "subdir/parent", + Linkname: "..", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + return &buff + +} + +func absoluteSymlink(t *testing.T) io.Reader { + var err error + var buff bytes.Buffer + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "a.lnk", + Linkname: "/etc/shadow", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + return &buff +} + +func directEscape(t *testing.T) io.Reader { + t.Helper() + + var err error + var buff bytes.Buffer + var msg = "I am free!!!" + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "../escaped.txt", + Typeflag: tar.TypeReg, + Mode: 0644, + Size: int64(len(msg)), + }) + if err != nil { + t.Fatal(err) + } + _, err = w.Write([]byte(msg)) + if err != nil { + t.Fatal(err) + } + return &buff +} + +func indirectLinkEscape(t *testing.T) io.Reader { + t.Helper() + t.Skip("we are not checking for this since the core utils tar does not too") + + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "subdir", + Typeflag: tar.TypeDir, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + err = w.WriteHeader(&tar.Header{ + Name: "subdir/parent", + Linkname: "..", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + err = w.WriteHeader(&tar.Header{ + Name: "escape", + Linkname: "subdir/parent/..", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + return &buff +} + +func indirectLinkEscapeWithOverwrites(t *testing.T) io.Reader { + t.Helper() + t.Skip("we are not checking for this since the core utils tar does not too") + + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "subdir", + Typeflag: tar.TypeDir, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "subdir/parent", + Typeflag: tar.TypeDir, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "escape", + Linkname: "subdir/parent/..", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "subdir/parent", + Linkname: "..", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + + return &buff +} + +func TestDumpTar(t *testing.T) { + f, err := os.OpenFile("/tmp/a.tar", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + defer func(f *os.File) { + _ = f.Close() + }(f) + _, err = io.Copy(f, doubleDotInName(t)) + if err != nil { + t.Fatal(err) + } +} + +func doubleDotInName(t *testing.T) io.Reader { + t.Helper() + + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "subdir", + Typeflag: tar.TypeDir, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + err = w.WriteHeader(&tar.Header{ + Name: "subdir/parent", + Typeflag: tar.TypeDir, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + err = w.WriteHeader(&tar.Header{ + Name: "subdir/parent/../../a.txt", + Typeflag: tar.TypeReg, + Mode: 0644, + Size: 0, + }) + if err != nil { + t.Fatal(err) + } + return &buff +} + +func missingParentRegular(t *testing.T) io.Reader { + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "missing/a.txt", + Typeflag: tar.TypeReg, + Size: 0, + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + + return &buff +} + +func missingParentLink(t *testing.T) io.Reader { + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "missing/a.lnk", + Linkname: "a.txt", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + + return &buff +} + +func overrideRegularFile(t *testing.T) io.Reader { + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + err = w.WriteHeader(&tar.Header{ + Name: "a.txt", + Typeflag: tar.TypeReg, + Size: 0, + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "a.txt", + Typeflag: tar.TypeReg, + Size: 0, + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + + return &buff +} + +func overrideLink(t *testing.T) io.Reader { + var err error + var buff bytes.Buffer + + w := tar.NewWriter(&buff) + defer func(w *tar.Writer) { + _ = w.Close() + }(w) + + err = w.WriteHeader(&tar.Header{ + Name: "a.txt", + Typeflag: tar.TypeReg, + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "b.txt", + Typeflag: tar.TypeReg, + Mode: 0644, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "a.lnk", + Linkname: "a.txt", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + + err = w.WriteHeader(&tar.Header{ + Name: "a.lnk", + Linkname: "b.txt", + Typeflag: tar.TypeSymlink, + Mode: 0777, + }) + if err != nil { + t.Fatal(err) + } + return &buff +}