From 54a06333acf413c54f171ee1acb1960912e35958 Mon Sep 17 00:00:00 2001 From: "evil.gen.sec" Date: Thu, 26 Mar 2026 07:53:09 +0545 Subject: [PATCH 1/2] build: prevent kodata symlinks from escaping the kodata root walkRecursive dereferences symlinks in the kodata/ directory when packing files into container image layers. Previously there was no check that the resolved path remained within the kodata root, so a symlink pointing at an arbitrary host path (e.g. ~/.ssh/id_rsa or /etc/passwd) would cause ko to silently pack the target file into the published image. Fix this by computing the canonical absolute path of the kodata root (using both filepath.EvalSymlinks and filepath.Abs to handle platforms where the temp/home directory itself is behind a symlink, such as macOS) and verifying after each filepath.EvalSymlinks call that the resolved target path has the kodata root as a prefix. Symlinks whose targets lie outside the root now produce an explicit error instead of leaking host files. The boundary check uses: absEvalPath != absKodataRoot && !strings.HasPrefix(absEvalPath, absKodataRoot+sep) This allows a symlink that resolves exactly to the kodata root itself while still rejecting targets that escape it. Add two unit tests: - TestWalkRecursiveSymlinkTraversal: symlink escaping kodata root is rejected; skipped on Windows if os.Symlink is unavailable. - TestWalkRecursiveSymlinkWithinKodata: symlink within kodata root is accepted and the symlink target's content is verified in the resulting tar archive; skipped on Windows if os.Symlink is unavailable. --- pkg/build/gobuild.go | 32 ++++++++++- pkg/build/gobuild_test.go | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 74dc73aa3e..0fd47837f9 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -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 @@ -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 { @@ -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. @@ -826,7 +842,17 @@ 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. + 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) { diff --git a/pkg/build/gobuild_test.go b/pkg/build/gobuild_test.go index c1b5395d33..02bf571df6 100644 --- a/pkg/build/gobuild_test.go +++ b/pkg/build/gobuild_test.go @@ -16,6 +16,7 @@ package build import ( "archive/tar" + "bytes" "context" "errors" "fmt" @@ -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) + } + + 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) + } +} From 43f22a55849c9e1e231ac8e844bdf6535db78b62 Mon Sep 17 00:00:00 2001 From: evilgensec Date: Thu, 26 Mar 2026 08:21:55 +0545 Subject: [PATCH 2/2] fix: handle missing kodata directory in tarKoData filepath.EvalSymlinks fails when the kodata directory does not exist. Guard the call with os.Stat so that the non-existent case falls through to walkRecursive, which already silently no-ops for missing roots via filepath.Walk semantics. Fixes test failures in TestGoBuildNoKoData, TestGoBuildConsistentMediaTypes, and TestDebugger. --- pkg/build/gobuild.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/build/gobuild.go b/pkg/build/gobuild.go index 0fd47837f9..4b04f68496 100644 --- a/pkg/build/gobuild.go +++ b/pkg/build/gobuild.go @@ -844,13 +844,19 @@ func (g *gobuild) tarKoData(ref reference, platform *v1.Platform) (*bytes.Buffer // Resolve the canonical absolute path of the kodata root so that symlink // targets resolved by filepath.EvalSymlinks can be compared consistently. - 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) + // 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) }