CVE-2026-42574: apko Symlink Tar Entry Escapes Build Root
A crafted .apk's TypeSymlink tar entry can redirect subsequent writes outside the apko build root, giving an attacker host-path write primitives during image construction.
A widely-used tool for building container images—the packaged software that runs on modern cloud servers—has a serious security flaw. Think of containers like shipping containers for software: they're supposed to be completely isolated and sealed. This vulnerability punches a hole in that seal.
Here's what's happening. When developers use apko to build these containers, the tool processes software packages to include in the image. A malicious package can secretly plant a symbolic link—essentially a shortcut—that points outside the container to the host computer running the build. Later operations then follow that shortcut and write files directly to the actual server, not the isolated container.
This is like a contractor breaking into your house by posing as a delivery person, then leaving a hidden tunnel behind so their accomplice can steal from your bedroom later.
The vulnerability affects versions of apko released between early 2023 and mid-2024. An attacker would need to trick developers into using a poisoned software package—either through a compromised third-party library or a supply chain attack. If successful, they could modify or install files on the server, potentially stealing secrets, installing backdoors, or sabotaging systems.
Who's at risk? Primarily software development teams and cloud infrastructure companies that use apko in their build pipelines. If you use commercial cloud services, your provider might use this tool, but you're only directly vulnerable if you personally run apko locally.
What to do about it: First, if you use apko, update immediately to version 1.2.6 or later. Second, only use software packages from trusted sources you control or have thoroughly vetted. Third, run your build processes in isolated environments so even if something goes wrong, damage stays contained. Check your vendor's security advisories to see if they've addressed this on their end.
Want the full technical analysis? Click "Technical" above.
CVE-2026-42574 is a path traversal vulnerability in apko (Chainguard's OCI image builder for Alpine/apk packages) affecting versions 0.14.8 through 1.2.5 (exclusive). The bug lives in apko's tar extraction path: when installing an .apk archive, apko processes tar entries sequentially without validating that a TypeSymlink entry's target resolves within the build root. A subsequent TypeDir or file-write entry can then traverse the attacker-planted symlink to reach an arbitrary host path writable by the build user. CVSS 7.5 (HIGH), no authentication required beyond the ability to supply a crafted .apk.
Root cause: apko's tar extraction loop installs TypeSymlink entries verbatim without checking that the symlink target resolves inside buildRoot, allowing a later entry in the same or subsequent archive to follow the symlink to an attacker-controlled host path.
Affected Component
Package: github.com/chainguard-dev/apko
Vulnerable range: >=0.14.8, <1.2.5
Fixed: v1.2.5
Entry point: the installAPK / tar-extraction layer inside pkg/apk/, specifically the loop that calls fs.Symlink for tar.TypeSymlink header types and subsequently fs.MkdirAll / file-write for directory and regular-file entries.
Root Cause Analysis
apko extracts .apk archives (which are gzip'd tar streams) entry-by-entry. The extraction loop switches on hdr.Typeflag. For TypeSymlink, it calls the underlying filesystem's Symlink(hdr.Linkname, target) directly. The link target is taken verbatim from the tar header — no filepath.Clean, no prefix check against the build root, no os.Lstat loop to resolve intermediate components.
// Pseudocode reconstruction of the vulnerable extraction loop
// Real Go path: pkg/apk/impl.go (pre-1.2.5)
func extractTar(fsys apkfs.FullFS, buildRoot string, tr *tar.Reader) error {
for {
hdr, err := tr.Next()
if err == io.EOF { break }
target := filepath.Join(buildRoot, hdr.Name)
switch hdr.Typeflag {
case tar.TypeSymlink:
// BUG: hdr.Linkname is attacker-controlled and never validated
// against buildRoot. An absolute path or deep "../../../" sequence
// silently escapes the build root.
err = fsys.Symlink(hdr.Linkname, target) // <-- BUG: no bounds check
case tar.TypeDir:
// If target itself is (or traverses) the rogue symlink planted
// above, MkdirAll follows it onto the host filesystem.
err = fsys.MkdirAll(target, hdr.FileInfo().Mode()) // BUG: follows symlinks
case tar.TypeReg:
// Same issue: Create opens the path, following any symlink
// component, writing attacker content to the resolved host path.
f, err := fsys.Create(target) // BUG: follows symlinks
io.Copy(f, tr)
}
}
}
The critical gap is the absence of a safe-join helper that, after resolving all symlink components, asserts the canonical path still has buildRoot as a prefix. Because filepath.Join is used naively and the OS transparently follows symlinks during open(2) / mkdir(2), the kernel does the traversal for free — from the attacker's perspective.
// Struct layout of a synthesized tar header used in the attack
// (encoding/tar internal representation, relevant fields)
struct tar_Header {
/* +0x00 */ char Name[100]; // entry path, e.g. "usr/lib/evil"
/* +0x64 */ uint32_t Mode;
/* +0x6C */ int64_t Size;
/* +0x74 */ uint8_t Typeflag; // 0x32='2' => TypeSymlink
/* +0x75 */ char Linkname[100]; // symlink target — UNCHECKED
// ...
};
// When Typeflag==TypeSymlink, Linkname="../../../../tmp" escapes buildRoot.
// No canonicalization occurs before fsys.Symlink() is invoked.
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker crafts a malicious .apk (gzip'd tar) with two entries:
Entry A — TypeSymlink
Name : "usr/share/apko-escape"
Linkname: "../../../../tmp" ← escapes buildRoot
(planted as buildRoot/usr/share/apko-escape -> /tmp on the host)
Entry B — TypeReg (or TypeDir)
Name : "usr/share/apko-escape/pwned"
Content : attacker-controlled payload (e.g., cron job, .bashrc, shared lib)
2. Attacker publishes/hosts the malicious .apk in a reachable apk repository
(or substitutes it via a MITM on an HTTP repository mirror — apko
does not enforce per-package signature verification before extraction
when the index is already trusted).
3. apko build process references the package in an apko.yaml:
contents:
packages:
- evil-pkg
4. apko calls installAPK() → extractTar():
a. Entry A processed: fsys.Symlink("../../../../tmp",
"/build/root/usr/share/apko-escape")
→ symlink created inside buildRoot pointing to /tmp (host)
b. Entry B processed: fsys.Create(
"/build/root/usr/share/apko-escape/pwned")
→ kernel resolves symlink → opens /tmp/pwned for writing
→ attacker content written to /tmp/pwned on the host
5. Depending on write target and build-user privileges:
- Overwrite ~/.bashrc, ~/.profile → code execution on next login
- Drop .so into /tmp, race LD_PRELOAD vectors
- Write to world-writable spool dirs (/var/spool/cron, etc.)
- In CI/CD pipelines running as root: arbitrary write to /etc/passwd,
/etc/sudoers, or build artifact output paths
Memory Layout
This is a logic/path traversal bug rather than a memory-corruption primitive, so the relevant "layout" is the filesystem namespace as seen by the extraction process:
FILESYSTEM STATE — build root at /workspace/build/root/
BEFORE malicious .apk extraction:
/workspace/build/root/
usr/
lib/
share/ ← legitimate directory
HOST (outside buildRoot):
/tmp/ ← build user has write access
AFTER Entry A (TypeSymlink) is processed:
/workspace/build/root/
usr/
share/
apko-escape → ../../../../tmp ← ROGUE SYMLINK (resolves to /tmp)
AFTER Entry B (TypeReg "usr/share/apko-escape/pwned") is processed:
/workspace/build/root/
usr/
share/
apko-escape → /tmp (symlink unchanged)
HOST (outside buildRoot):
/tmp/
pwned ← ATTACKER-CONTROLLED FILE WRITTEN TO HOST PATH
Effective open(2) call chain for Entry B:
open("/workspace/build/root/usr/share/apko-escape/pwned", O_CREAT|O_WRONLY)
→ kernel resolves "apko-escape" component → follows symlink to /tmp
→ kernel opens "/tmp/pwned" ← host path, outside build jail
Patch Analysis
The fix in v1.2.5 introduces a safe-join / symlink-escape check. Before any Symlink, MkdirAll, or file-write call, the resolved canonical path is verified to share the buildRoot prefix. Additionally, symlink targets are validated at plant-time — absolute targets and targets that escape the build root are rejected outright.
// BEFORE (vulnerable, pre-1.2.5):
case tar.TypeSymlink:
target := filepath.Join(buildRoot, hdr.Name)
err = fsys.Symlink(hdr.Linkname, target) // no validation of Linkname
case tar.TypeDir:
target := filepath.Join(buildRoot, hdr.Name)
err = fsys.MkdirAll(target, mode) // blindly follows any symlink component
case tar.TypeReg:
target := filepath.Join(buildRoot, hdr.Name)
f, err := fsys.Create(target) // same
// AFTER (patched, v1.2.5):
// safeJoin resolves the final path and asserts prefix == buildRoot
func safeJoin(root, untrusted string) (string, error) {
joined := filepath.Join(root, untrusted)
// filepath.EvalSymlinks would follow existing links;
// instead walk each component manually:
clean := filepath.Clean(joined)
if !strings.HasPrefix(clean+string(os.PathSeparator),
filepath.Clean(root)+string(os.PathSeparator)) {
return "", fmt.Errorf("path %q escapes build root", untrusted)
}
return clean, nil
}
case tar.TypeSymlink:
target, err := safeJoin(buildRoot, hdr.Name)
if err != nil { return err }
// BUG FIX: validate that the symlink *target* also resolves inside root
// when treated as relative to the entry's parent directory.
linkAbs := filepath.Join(filepath.Dir(target), hdr.Linkname)
if _, err := safeJoin(buildRoot, linkAbs); err != nil {
return fmt.Errorf("symlink target escapes build root: %w", err)
}
err = fsys.Symlink(hdr.Linkname, target)
case tar.TypeDir:
target, err := safeJoin(buildRoot, hdr.Name)
if err != nil { return err }
// Additionally: resolve through existing symlinks before mkdir
err = fsys.MkdirAll(target, mode)
case tar.TypeReg:
target, err := safeJoin(buildRoot, hdr.Name)
if err != nil { return err }
f, err := fsys.Create(target)
The patch also hardens hardlink entries (TypeLink) with the same safeJoin guard — a related vector that was silently closed in the same commit.
Detection and Indicators
Detection during a build:
Audit tar entries in installed .apk files for TypeSymlink entries whose Linkname is absolute (/-prefixed) or contains a ../ sequence that reaches above the archive root.
Enable filesystem auditing (auditd, inotifywait, fanotify) on the build host — any open(2) / creat(2) outside the declared buildRoot during apko build is anomalous.
Post-build: compare the set of files written during the build (via strace -e trace=openat,symlinkat) against the expected build root subtree.
Artifact to search for in existing .apk files:
import tarfile, gzip, sys
def audit_apk(path):
with gzip.open(path) as gz:
with tarfile.open(fileobj=gz) as tf:
for m in tf.getmembers():
if m.issym():
target = m.linkname
if target.startswith('/') or '../' in target:
print(f"[SUSPICIOUS] {path}: symlink {m.name!r} -> {target!r}")
for apk in sys.argv[1:]:
audit_apk(apk)
Remediation
Upgrade apko to v1.2.5 or later. This is the only complete fix.
If upgrading immediately is not possible: run apko builds inside a user-namespace container (rootless podman, bubblewrap) or chroot jail so the build user cannot write to sensitive host paths even if a symlink escapes the build root.
Apply strict repository pinning and verify .apk signatures against a known-good key before invoking apko — this raises the bar for delivering a malicious package through a trusted mirror.
In CI/CD pipelines: audit apko.yaml package lists for third-party or community repositories; prefer Chainguard's signed, reproducible packages where the full provenance chain is verifiable via Sigstore/Cosign.