Skip to content
Merged
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
38 changes: 35 additions & 3 deletions pkg/build/gobuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,10 @@ func writeFileToTar(tw *tar.Writer, name, evalPath string, size int64, modTime t
// walkRecursive performs a filepath.Walk of the given root directory adding it
// to the provided tar.Writer with root -> chroot. All symlinks are dereferenced,
// which is what leads to recursion when we encounter a directory symlink.
func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time, platform *v1.Platform) error {
// absKodataRoot is the absolute path of the original kodata directory; symlinks
// that resolve to a path outside of it are rejected to prevent arbitrary host
// files from being packed into the container image.
func walkRecursive(tw *tar.Writer, root, chroot, absKodataRoot string, creationTime v1.Time, platform *v1.Platform) error {
return filepath.Walk(root, func(hostPath string, info os.FileInfo, err error) error {
if hostPath == root {
return nil
Expand Down Expand Up @@ -769,6 +772,19 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time, pl
return fmt.Errorf("filepath.EvalSymlinks(%q): %w", hostPath, err)
}

// Verify the resolved path remains within the kodata root. A symlink
// pointing outside the kodata directory would otherwise cause ko to pack
// arbitrary host files (e.g. ~/.ssh/id_rsa, /etc/passwd) into the image.
absEvalPath, err := filepath.Abs(evalPath)
if err != nil {
return fmt.Errorf("filepath.Abs(%q): %w", evalPath, err)
}
absKodataRootWithSep := absKodataRoot + string(filepath.Separator)
if absEvalPath != absKodataRoot && !strings.HasPrefix(absEvalPath, absKodataRootWithSep) {
return fmt.Errorf("kodata symlink %q resolves to %q which is outside the kodata root %q",
hostPath, evalPath, absKodataRoot)
}

// Get info of the symlink target.
info, err = os.Stat(evalPath)
if err != nil {
Expand All @@ -780,7 +796,7 @@ func walkRecursive(tw *tar.Writer, root, chroot string, creationTime v1.Time, pl
if err := writeDirToTar(tw, newPath, creationTime.Time); err != nil {
return fmt.Errorf("writing dir %q to tar: %w", newPath, err)
}
return walkRecursive(tw, evalPath, newPath, creationTime, platform)
return walkRecursive(tw, evalPath, newPath, absKodataRoot, creationTime, platform)
}

// Regular file (or symlink to file): write to tar.
Expand Down Expand Up @@ -826,7 +842,23 @@ func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer
}
}

return buf, walkRecursive(tw, root, chroot, creationTime, platform)
// Resolve the canonical absolute path of the kodata root so that symlink
// targets resolved by filepath.EvalSymlinks can be compared consistently.
// If the kodata directory does not exist, walkRecursive handles that
// gracefully (filepath.Walk skips missing roots), so we fall back to the
// raw path as the boundary — no traversal is possible without a root.
absKodataRoot := root
if _, statErr := os.Stat(root); statErr == nil {
resolvedRoot, err := filepath.EvalSymlinks(root)
if err != nil {
return nil, fmt.Errorf("filepath.EvalSymlinks(%q): %w", root, err)
}
absKodataRoot, err = filepath.Abs(resolvedRoot)
if err != nil {
return nil, fmt.Errorf("filepath.Abs(%q): %w", resolvedRoot, err)
}
}
return buf, walkRecursive(tw, root, chroot, absKodataRoot, creationTime, platform)
}

func createTemplateData(ctx context.Context, buildCtx buildContext) (map[string]any, error) {
Expand Down
114 changes: 114 additions & 0 deletions pkg/build/gobuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package build

import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
Expand Down Expand Up @@ -1633,3 +1634,116 @@ func TestDebugger(t *testing.T) {
}
}
}

// TestWalkRecursiveSymlinkTraversal verifies that walkRecursive rejects symlinks
// in kodata/ that point outside the kodata root, preventing host files from being
// packed into the container image.
func TestWalkRecursiveSymlinkTraversal(t *testing.T) {
// Build a temp kodata dir with a symlink that escapes it.
kodataDir := t.TempDir()
outsideDir := t.TempDir()

// Write a sensitive file outside kodata.
sensitiveFile := filepath.Join(outsideDir, "secret.txt")
if err := os.WriteFile(sensitiveFile, []byte("supersecret"), 0600); err != nil {
t.Fatal(err)
}

// Create a symlink inside kodata pointing to the file outside kodata.
escapingLink := filepath.Join(kodataDir, "escape")
if err := os.Symlink(sensitiveFile, escapingLink); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("skipping symlink traversal test on Windows: %v", err)
}
t.Fatal(err)
}
Comment thread
evilgensec marked this conversation as resolved.

resolvedKodata, err := filepath.EvalSymlinks(kodataDir)
if err != nil {
t.Fatal(err)
}
absKodataRoot, err := filepath.Abs(resolvedKodata)
if err != nil {
t.Fatal(err)
}

var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
platform := &v1.Platform{OS: "linux", Architecture: "amd64"}
creationTime := v1.Time{}

err = walkRecursive(tw, kodataDir, "/var/run/ko", absKodataRoot, creationTime, platform)
if err == nil {
t.Error("walkRecursive: expected error for symlink escaping kodata root, got nil")
} else if !strings.Contains(err.Error(), "outside the kodata root") {
t.Errorf("walkRecursive: unexpected error message: %v", err)
}
}

// TestWalkRecursiveSymlinkWithinKodata verifies that symlinks within kodata are
// still followed correctly after the traversal check is applied.
func TestWalkRecursiveSymlinkWithinKodata(t *testing.T) {
kodataDir := t.TempDir()

// Write a regular file and a symlink to it, both inside kodata.
target := filepath.Join(kodataDir, "target.txt")
if err := os.WriteFile(target, []byte("hello"), 0644); err != nil {
t.Fatal(err)
}
link := filepath.Join(kodataDir, "link.txt")
if err := os.Symlink(target, link); err != nil {
if runtime.GOOS == "windows" {
t.Skipf("skipping symlink within-kodata test on Windows: %v", err)
}
t.Fatal(err)
}

resolvedKodata, err := filepath.EvalSymlinks(kodataDir)
if err != nil {
t.Fatal(err)
}
absKodataRoot, err := filepath.Abs(resolvedKodata)
if err != nil {
t.Fatal(err)
}

var buf bytes.Buffer
tw := tar.NewWriter(&buf)
defer tw.Close()
platform := &v1.Platform{OS: "linux", Architecture: "amd64"}
creationTime := v1.Time{}

if err := walkRecursive(tw, kodataDir, "/var/run/ko", absKodataRoot, creationTime, platform); err != nil {
t.Fatalf("walkRecursive: unexpected error for in-kodata symlink: %v", err)
}
tw.Close()

// Verify that the symlink target was actually written to the tar archive.
wantPath := path.Join("/var/run/ko", "link.txt")
found := false
tr := tar.NewReader(&buf)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("tar.Reader.Next(): %v", err)
}
if hdr.Name != wantPath {
continue
}
found = true
body, err := io.ReadAll(tr)
if err != nil {
t.Fatalf("io.ReadAll: %v", err)
}
if got, want := string(body), "hello"; got != want {
t.Errorf("link.txt content = %q, want %q", got, want)
}
}
if !found {
t.Errorf("expected %q in tar archive, not found", wantPath)
}
}
Loading