home intel cve-2026-42574-apko-symlink-tar-traversal
CVE Analysis 2026-05-09 · 8 min read

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.

#path-traversal#symlink-attack#arbitrary-file-write#container-escape#apk-package
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-42574 · Vulnerability
ATTACKERCross-platformVULNERABILITYCVE-2026-42574HIGHSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →