build: allow kodata symlinks within the source tree#1699
Conversation
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>
There was a problem hiding this comment.
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
resolveKodataAllowedRootto derive the allowed root fromKO_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.
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>
|
I had Copilot throw #1698 together -- I haven't reviewed it yet myself but I will probably later today. |
evankanderson
left a comment
There was a problem hiding this comment.
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!
- 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>
|
Thanks for the review @evankanderson! Pushed d455fba addressing all of it:
Let me know if you'd prefer to drop the git auto-detection entirely and lean on |
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-levelLICENSE, shared config, etc.).This PR keeps the security property (a symlink to
~/.ssh/id_rsaor/etc/passwdstill 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.gowalkRecursive's boundary parameter is renamedabsKodataRoot->absAllowedRoot; theEvalSymlinks+Abs+HasPrefixcontainment check now runs against this wider root. The recursion for directory symlinks passes it through unchanged, so the check holds at every nesting level.resolveKodataAllowedRootcomputes the allowed root:KO_DATA_PATH_ALLOWED_ROOTwhen set (for monorepos / non-git builds),git rev-parse --show-toplevel,tarKoDatacomputesabsAllowedRootand passes it towalkRecursive.pkg/build/gobuild_test.goTestWalkRecursiveSymlinkWithinAllowedRoot: a symlink in kodata pointing to an in-repo, out-of-kodataLICENSEis 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.TestWalkRecursiveSymlinkTraversal/TestWalkRecursiveSymlinkWithinKodatastill pass (error message updated to "outside the allowed root").Test
Full
go test ./pkg/build/passes.Closes the compatibility regression raised in #1619 without reverting the symlink-escape fix.