Skip to content

build: prevent kodata symlinks from escaping the kodata root#1619

Merged
imjasonh merged 2 commits into
ko-build:mainfrom
evilgensec:fix/kodata-symlink-traversal
Apr 1, 2026
Merged

build: prevent kodata symlinks from escaping the kodata root#1619
imjasonh merged 2 commits into
ko-build:mainfrom
evilgensec:fix/kodata-symlink-traversal

Conversation

@evilgensec

@evilgensec evilgensec commented Mar 26, 2026

Copy link
Copy Markdown
Contributor

Summary

walkRecursive dereferences symlinks in kodata/ 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's kodata/ 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.go

  • walkRecursive gains an absKodataRoot string parameter: the canonical absolute path of the original kodata directory (resolved with both filepath.EvalSymlinks and filepath.Abs).
  • After filepath.EvalSymlinks(hostPath), the resolved path is checked with strings.HasPrefix(absEvalPath, absKodataRoot+string(filepath.Separator)). If the target is outside the kodata root the walk returns an error.
  • The recursive call for directory symlinks passes the same absKodataRoot so the check is enforced at all nesting levels.
  • tarKoData computes absKodataRoot using filepath.EvalSymlinks then filepath.Abs (order matters on macOS where /var/private/var) and passes it to walkRecursive.

pkg/build/gobuild_test.go

  • TestWalkRecursiveSymlinkTraversal: symlink escaping kodata root returns an error.
  • TestWalkRecursiveSymlinkWithinKodata: symlink within kodata root is still followed correctly.

Test

go test ./pkg/build/ -run TestWalkRecursiveSymlink -v
=== RUN   TestWalkRecursiveSymlinkTraversal
--- PASS
=== RUN   TestWalkRecursiveSymlinkWithinKodata
--- PASS

Related security report: https://issuetracker.google.com/issues/495623327

Copilot AI review requested due to automatic review settings March 26, 2026 02:08

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 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 walkRecursive with an absKodataRoot parameter and reject symlink targets that resolve outside the kodata root.
  • Canonicalize kodata root in tarKoData using filepath.EvalSymlinks then filepath.Abs and 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.

Comment thread pkg/build/gobuild.go Outdated
Comment thread pkg/build/gobuild_test.go
Comment thread pkg/build/gobuild_test.go Outdated
Comment thread pkg/build/gobuild_test.go Outdated
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.
@evilgensec evilgensec force-pushed the fix/kodata-symlink-traversal branch from 723719a to 54a0633 Compare March 26, 2026 02:15
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.
@imjasonh

Copy link
Copy Markdown
Member

Related security report: https://issuetracker.google.com/issues/495623327

FYI I can't view this security report.

@evilgensec

Copy link
Copy Markdown
Contributor Author

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?

Screenshot 2026-03-26 at 08 33 57

@evilgensec

Copy link
Copy Markdown
Contributor Author

Dear @imjasonh, hope you are doing well. Is there any update on the issue?

@imjasonh imjasonh merged commit 2c794ac into ko-build:main Apr 1, 2026
18 of 19 checks passed
Madh93 added a commit to Madh93/karakeepbot that referenced this pull request Jun 26, 2026
realshuting added a commit to kyverno/kyverno that referenced this pull request Jun 29, 2026
…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>
khrm added a commit to khrm/chains that referenced this pull request Jun 29, 2026
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>
khrm added a commit to khrm/pipeline that referenced this pull request Jun 29, 2026
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>
tekton-robot pushed a commit to tektoncd/triggers that referenced this pull request Jun 29, 2026
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>
khrm added a commit to khrm/chains that referenced this pull request Jun 29, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jun 29, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jun 29, 2026
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>
khrm added a commit to khrm/chains that referenced this pull request Jun 29, 2026
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>
khrm added a commit to khrm/chains that referenced this pull request Jun 30, 2026
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>
khrm added a commit to khrm/chains that referenced this pull request Jun 30, 2026
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>
khrm added a commit to khrm/chains that referenced this pull request Jun 30, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 3, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 3, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 3, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 3, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 3, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 3, 2026
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

Copy link
Copy Markdown
Contributor

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?

@imjasonh

imjasonh commented Jul 6, 2026

Copy link
Copy Markdown
Member

Yeah I'd accept that. We can also revert the change if it's a problem.

tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 6, 2026
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>
tekton-robot pushed a commit to tektoncd/pipeline that referenced this pull request Jul 6, 2026
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>
@evilgensec

Copy link
Copy Markdown
Contributor Author

@evankanderson @imjasonh good point, and reverting would re-open the exfil hole (a symlink to ~/.ssh/id_rsa or /etc/passwd getting packed into the pushed image). I'd rather fix the breakage than roll it back.

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.

  • Resolve the allowed parent as the git top-level (git rev-parse --show-toplevel) from the importpath dir; fall back to the kodata root when it isn't a git checkout.
  • Keep the existing EvalSymlinks + Abs + HasPrefix check, just against the wider root.
  • Add an override env (e.g. KO_DATA_PATH_ALLOWED_ROOT) for monorepos / non-git builds.

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.

@evankanderson

Copy link
Copy Markdown
Contributor

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.

@evilgensec

Copy link
Copy Markdown
Contributor Author

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.

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