Skip to content

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

Open
evilgensec wants to merge 2 commits intoko-build:mainfrom
evilgensec:fix/kodata-symlink-traversal
Open

build: prevent kodata symlinks from escaping the kodata root#1619
evilgensec wants to merge 2 commits intoko-build:mainfrom
evilgensec:fix/kodata-symlink-traversal

Conversation

@evilgensec
Copy link
Copy Markdown

@evilgensec evilgensec commented Mar 26, 2026

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
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.

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
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

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.

3 participants