build: prevent kodata symlinks from escaping the kodata root#1619
Conversation
There was a problem hiding this comment.
Pull request overview
This PR mitigates a symlink traversal vulnerability in kodata/ packaging by ensuring dereferenced symlink targets remain within the original kodata root before adding content to image layers.
Changes:
- Extend
walkRecursivewith anabsKodataRootparameter and reject symlink targets that resolve outside the kodata root. - Canonicalize
kodataroot intarKoDatausingfilepath.EvalSymlinksthenfilepath.Absand pass it through recursion. - Add tests covering escaping vs. in-root symlink behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| pkg/build/gobuild.go | Adds canonical-root computation and enforces “resolved path must be within kodata root” during recursive tar packing. |
| pkg/build/gobuild_test.go | Adds regression tests for escaping symlinks and expected behavior for in-root symlinks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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.
723719a to
54a0633
Compare
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.
FYI I can't view this security report. |
|
Dear @imjasonh, I received the following message while reporting a security issue. Is there any further way I can disclose the issue to the team responsibly?
|
|
Dear @imjasonh, hope you are doing well. Is there any update on the issue? |
…16381) * fix: upgrade go toolchain to 1.26.4 go 1.26.4 (2026-06-02) is the current latest stable release and includes all security fixes from 1.26.3 (CVE-2026-39836) plus additional fixes to the compiler, runtime, crypto/fips140 and crypto/x509 packages. Signed-off-by: realshuting <shuting@nirmata.com> * fix: replace kodata symlinks with actual files ko v0.19.0 introduced a security check that rejects kodata symlinks resolving outside the kodata root (ko-build/ko#1619). All kodata directories had relative symlinks pointing to the root LICENSE and NOTICE files: LICENSE -> ../../../LICENSE NOTICE -> ../../../NOTICE Replace these symlinks with actual file copies so ko can include them in the image without violating the security boundary. Signed-off-by: realshuting <shuting@nirmata.com> * fix: correct toolchain bump to go 1.26.4 Previous commit on this branch accidentally applied a reverse diff (1.26.3 -> 1.26.2) while carrying an incorrect message. Set all intended versions to 1.26.4 in root and hack modules, and align the argocd Kind image default to v1.26.4. Signed-off-by: realshuting <shuting@nirmata.com> --------- Signed-off-by: realshuting <shuting@nirmata.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing symlinks in cmd/controller/kodata/ all resolve outside the root: - LICENSE -> ../../../LICENSE (resolved to repo root) - HEAD -> ../../../.git/HEAD (resolved to .git) - refs -> ../../../.git/refs (resolved to .git) Replace the LICENSE symlink with an actual copy of the LICENSE file. Remove HEAD and refs entirely — they are not read by the controller at runtime and are not sustainable to copy (change on every commit). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced two breaking changes: 1. Strict kodata symlink enforcement (ko-build/ko#1619) 2. A YAML encoder error when a label selector filters all documents in a file (fixed in ko-build/ko fix-empty-docnodes-encoder-close) Pin to v0.18.1 as a temporary workaround until ko releases a fixed version. The kodata symlinks are also fixed in this branch. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing LICENSE -> ../../../LICENSE symlinks in cmd/*/kodata/ resolve to the repo root, which ko v0.19.0 now rejects when tarring for any non-kind registry, breaking CI. Replace the three symlinks with actual copies of the LICENSE file: - cmd/controller/kodata/LICENSE - cmd/eventlistenersink/kodata/LICENSE - cmd/webhook/kodata/LICENSE Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing symlinks in cmd/controller/kodata/ all resolve outside the root: - LICENSE -> ../../../LICENSE (resolved to repo root) - HEAD -> ../../../.git/HEAD (resolved to .git) - refs -> ../../../.git/refs (resolved to .git) Replace the LICENSE symlink with an actual copy of the LICENSE file. Remove HEAD and refs entirely — they are not read by the controller at runtime and are not sustainable to copy (change on every commit). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing LICENSE -> ../../../LICENSE symlinks in all cmd/*/kodata/ directories resolve to the repo root, which ko now rejects when tarring for any non-kind registry (including CI). Replace the symlinks with actual copies of the LICENSE file. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced two breaking changes: 1. Strict kodata symlink enforcement (ko-build/ko#1619) 2. A YAML encoder error when a label selector filters all documents in a file (fixed in ko-build/ko fix-empty-docnodes-encoder-close) Pin to v0.18.1 as a temporary workaround until ko releases a fixed version. The kodata symlinks are also fixed in this branch. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing symlinks in cmd/controller/kodata/ all resolve outside the root: - LICENSE -> ../../../LICENSE (resolved to repo root) - HEAD -> ../../../.git/HEAD (resolved to .git) - refs -> ../../../.git/refs (resolved to .git) Replace the LICENSE symlink with an actual copy of the LICENSE file. Remove HEAD and refs entirely — they are not read by the controller at runtime and are not sustainable to copy (change on every commit). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing symlinks in cmd/controller/kodata/ all resolve outside the root: - LICENSE -> ../../../LICENSE (resolved to repo root) - HEAD -> ../../../.git/HEAD (resolved to .git) - refs -> ../../../.git/refs (resolved to .git) Replace the LICENSE symlink with an actual copy of the LICENSE file. Remove HEAD and refs entirely — they are not read by the controller at runtime and are not sustainable to copy (change on every commit). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing symlinks in cmd/controller/kodata/ all resolve outside the root: - LICENSE -> ../../../LICENSE (resolved to repo root) - HEAD -> ../../../.git/HEAD (resolved to .git) - refs -> ../../../.git/refs (resolved to .git) Replace the LICENSE symlink with an actual copy of the LICENSE file. Remove HEAD and refs entirely — they are not read by the controller at runtime and are not sustainable to copy (change on every commit). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing LICENSE -> ../../../LICENSE symlinks in all cmd/*/kodata/ directories resolve to the repo root, which ko now rejects when tarring for any non-kind registry (including CI). Replace the symlinks with actual copies of the LICENSE file. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced two breaking changes: 1. Strict kodata symlink enforcement (ko-build/ko#1619) 2. A YAML encoder error when a label selector filters all documents in a file (fixed in ko-build/ko fix-empty-docnodes-encoder-close) Pin to v0.18.1 as a temporary workaround until ko releases a fixed version. The kodata symlinks are also fixed in this branch. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing LICENSE -> ../../../LICENSE symlinks in all cmd/*/kodata/ directories resolve to the repo root, which ko now rejects when tarring for any non-kind registry (including CI). Replace the symlinks with actual copies of the LICENSE file. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced two breaking changes: 1. Strict kodata symlink enforcement (ko-build/ko#1619) 2. A YAML encoder error when a label selector filters all documents in a file (fixed in ko-build/ko fix-empty-docnodes-encoder-close) Pin to v0.18.1 as a temporary workaround until ko releases a fixed version. The kodata symlinks are also fixed in this branch. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing LICENSE -> ../../../LICENSE symlinks in all cmd/*/kodata/ directories resolve to the repo root, which ko now rejects when tarring for any non-kind registry (including CI). Replace the symlinks with actual copies of the LICENSE file. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced two breaking changes: 1. Strict kodata symlink enforcement (ko-build/ko#1619) 2. A YAML encoder error when a label selector filters all documents in a file (fixed in ko-build/ko fix-empty-docnodes-encoder-close) Pin to v0.18.1 as a temporary workaround until ko releases a fixed version. The kodata symlinks are also fixed in this branch. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
FWIW, this seems likely to be breaking for a number of projects that symlink to a file such as LICENSE or other configuration that is in the project repository, but outside the command root. Would a patch to define an "allowed parent directory" (and/or pick one out of git automatically) be accepted? |
|
Yeah I'd accept that. We can also revert the change if it's a problem. |
ko v0.19.0 introduced strict enforcement that kodata symlinks must not resolve outside the kodata root (ko-build/ko#1619). The existing LICENSE -> ../../../LICENSE symlinks in all cmd/*/kodata/ directories resolve to the repo root, which ko now rejects when tarring for any non-kind registry (including CI). Replace the symlinks with actual copies of the LICENSE file. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
ko v0.19.0 introduced two breaking changes: 1. Strict kodata symlink enforcement (ko-build/ko#1619) 2. A YAML encoder error when a label selector filters all documents in a file (fixed in ko-build/ko fix-empty-docnodes-encoder-close) Pin to v0.18.1 as a temporary workaround until ko releases a fixed version. The kodata symlinks are also fixed in this branch. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
|
@evankanderson @imjasonh good point, and reverting would re-open the exfil hole (a symlink to Proposal: widen the allowed boundary from the kodata root to the project's git repo root, so in-repo symlinks (LICENSE, shared config outside the command dir) keep working while out-of-repo targets stay blocked.
Tests: in-repo-but-outside-kodata symlink is allowed; outside-repo symlink still errors. The git checkout is already trusted build context, so treating the repo root as the trust boundary keeps the security property while removing the false positive. Happy to send a PR if that direction works for you. |
|
I'm not going to fight you to get a PR in; I will probably have time to do it Wednesday or so if I haven't seen a PR by then. The spec you laid out looks correct for the cases I've thought of. |
|
Opened #1699 with the git-root allowed-parent approach we discussed (env override + git worktree fallback, ancestor-only so it never narrows below kodata). @evankanderson that should cover the in-repo LICENSE/config symlink case without reverting the escape check. Thanks for the review. |

Summary
walkRecursivedereferences symlinks inkodata/when packing files into container image layers. 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,/etc/passwd,/root/.ssh/authorized_keys) would silently pack the target file into the published container image.A module maintainer or CI pipeline operator who can place a symlink in
kodata/— including via a dependency'skodata/directory or a compromised build step — could cause credentials and secrets from the build host to be embedded in the image pushed to a registry.Changes
pkg/build/gobuild.gowalkRecursivegains anabsKodataRoot stringparameter: the canonical absolute path of the original kodata directory (resolved with bothfilepath.EvalSymlinksandfilepath.Abs).filepath.EvalSymlinks(hostPath), the resolved path is checked withstrings.HasPrefix(absEvalPath, absKodataRoot+string(filepath.Separator)). If the target is outside the kodata root the walk returns an error.absKodataRootso the check is enforced at all nesting levels.tarKoDatacomputesabsKodataRootusingfilepath.EvalSymlinksthenfilepath.Abs(order matters on macOS where/var→/private/var) and passes it towalkRecursive.pkg/build/gobuild_test.goTestWalkRecursiveSymlinkTraversal: symlink escaping kodata root returns an error.TestWalkRecursiveSymlinkWithinKodata: symlink within kodata root is still followed correctly.Test
Related security report: https://issuetracker.google.com/issues/495623327