Skip to content

build: allow kodata symlinks within the source tree#1699

Open
evilgensec wants to merge 3 commits into
ko-build:mainfrom
evilgensec:build-kodata-symlink-allowed-root
Open

build: allow kodata symlinks within the source tree#1699
evilgensec wants to merge 3 commits into
ko-build:mainfrom
evilgensec:build-kodata-symlink-allowed-root

Conversation

@evilgensec

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #1619. That change rejected any kodata/ symlink whose target resolved outside the kodata directory. As @evankanderson noted on #1619, this also breaks the legitimate and common case of symlinking a file that lives inside the project repository but outside the command root (a top-level LICENSE, shared config, etc.).

This PR keeps the security property (a symlink to ~/.ssh/id_rsa or /etc/passwd still cannot be packed into the image) while removing the false positive, by widening the allowed boundary from the kodata root to the enclosing source tree.

Changes

pkg/build/gobuild.go

  • walkRecursive's boundary parameter is renamed absKodataRoot -> absAllowedRoot; the EvalSymlinks + Abs + HasPrefix containment check now runs against this wider root. The recursion for directory symlinks passes it through unchanged, so the check holds at every nesting level.
  • New resolveKodataAllowedRoot computes the allowed root:
    • KO_DATA_PATH_ALLOWED_ROOT when set (for monorepos / non-git builds),
    • otherwise the enclosing git worktree via git rev-parse --show-toplevel,
    • falling back to the kodata root when neither is available.
    • The candidate is only used when it is an ancestor of (or equal to) the kodata root, so the boundary can never be narrowed below kodata and normal kodata contents are never affected.
  • tarKoData computes absAllowedRoot and passes it to walkRecursive.

pkg/build/gobuild_test.go

  • TestWalkRecursiveSymlinkWithinAllowedRoot: a symlink in kodata pointing to an in-repo, out-of-kodata LICENSE is followed and packed (the case from build: prevent kodata symlinks from escaping the kodata root #1619).
  • TestResolveKodataAllowedRoot: ancestor override widens the boundary; a non-ancestor override and a non-git tree both fall back to the kodata root.
  • Existing TestWalkRecursiveSymlinkTraversal / TestWalkRecursiveSymlinkWithinKodata still pass (error message updated to "outside the allowed root").

Test

go test ./pkg/build/ -run 'TestWalkRecursiveSymlink|TestResolveKodataAllowedRoot' -v
=== RUN   TestWalkRecursiveSymlinkTraversal
--- PASS
=== RUN   TestWalkRecursiveSymlinkWithinKodata
--- PASS
=== RUN   TestWalkRecursiveSymlinkWithinAllowedRoot
--- PASS
=== RUN   TestResolveKodataAllowedRoot
--- PASS

Full go test ./pkg/build/ passes.

Closes the compatibility regression raised in #1619 without reverting the symlink-escape fix.

The kodata symlink escape check added in ko-build#1619 rejected any symlink that
resolved outside the kodata directory. This also broke the common case of
symlinking a file that lives in the project repo but outside the command
root (a top-level LICENSE, shared config, etc.), as reported on ko-build#1619.

Widen the boundary from the kodata root to the enclosing source tree: the
allowed root is taken from KO_DATA_PATH_ALLOWED_ROOT when set, otherwise
from the enclosing git worktree (git rev-parse --show-toplevel), falling
back to the kodata root when neither is available. The candidate is only
used when it is an ancestor of the kodata root, so the boundary can never
be narrowed below kodata. Symlinks that escape the source tree entirely
(e.g. ~/.ssh/id_rsa, /etc/passwd) are still rejected.

Add TestWalkRecursiveSymlinkWithinAllowedRoot and TestResolveKodataAllowedRoot.

Signed-off-by: evilgensec <evil.gen.sec@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts ko’s kodata/ symlink dereference security boundary to allow symlinks that point elsewhere within the same source tree (e.g., top-level LICENSE), while still preventing symlinks from escaping the project and accidentally packaging arbitrary host files into container images.

Changes:

  • Widened the symlink containment boundary from “kodata root only” to an “allowed root” that can be the enclosing source tree.
  • Added resolveKodataAllowedRoot to derive the allowed root from KO_DATA_PATH_ALLOWED_ROOT, git worktree top-level, or fallback to kodata root.
  • Added tests for “in-repo but out-of-kodata” symlinks and for allowed-root resolution behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
pkg/build/gobuild.go Widen symlink resolution boundary and add helper to compute the allowed root (env/git/fallback).
pkg/build/gobuild_test.go Add coverage for allowed-root symlink behavior and allowed-root resolution logic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/build/gobuild.go Outdated
Comment thread pkg/build/gobuild.go Outdated
Address Copilot review on ko-build#1699: when the allowed root already ends in a
path separator (a filesystem root such as "/" or a Windows drive root like
C:\), appending filepath.Separator produced a double-separator prefix and
HasPrefix spuriously rejected valid in-root symlinks.

Extract a withinRoot helper that only appends the separator when the root
does not already end in one, and use it in both walkRecursive and
resolveKodataAllowedRoot. Add TestWithinRoot covering the trailing-separator
root case.

Signed-off-by: evilgensec <evil.gen.sec@gmail.com>
@imjasonh

imjasonh commented Jul 6, 2026

Copy link
Copy Markdown
Member

I had Copilot throw #1698 together -- I haven't reviewed it yet myself but I will probably later today.

@evankanderson evankanderson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A couple comments, but for me it's faster to review a solution than to decide which solution to suggest, so thanks for the PR!

Comment thread pkg/build/gobuild.go
Comment thread pkg/build/gobuild.go Outdated
Comment thread pkg/build/gobuild.go
Comment thread pkg/build/gobuild_test.go
Comment thread pkg/build/gobuild.go Outdated
- withinRoot now uses filepath.Rel instead of manual prefix handling; robust
  to trailing and platform-specific separators.
- Replace the 'git rev-parse --show-toplevel' shell-out with a filesystem
  walk-up for a .git entry (enclosingGitRoot); no subprocess.
- Fix ineffectual assignment to absKodataRoot in tarKoData.
- Add TestResolveKodataAllowedRootGitWorktree.

Signed-off-by: evilgensec <evil.gen.sec@gmail.com>
@evilgensec

Copy link
Copy Markdown
Contributor Author

Thanks for the review @evankanderson! Pushed d455fba addressing all of it:

  • withinRoot now uses filepath.Rel
  • replaced the git shell-out with a filesystem .git walk-up (enclosingGitRoot), no subprocess
  • fixed the ineffassign lint failure
  • added a test for the git-worktree detection

Let me know if you'd prefer to drop the git auto-detection entirely and lean on KO_DATA_PATH_ALLOWED_ROOT only.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants