Skip to content

fix: use filepath.EvalSymlinks to detect symlink chain escapes in archive extraction#565

Open
mwentzell-block wants to merge 2 commits intocashapp:masterfrom
mwentzell-block:fix/symlink-chain-escape-vuln-78098
Open

fix: use filepath.EvalSymlinks to detect symlink chain escapes in archive extraction#565
mwentzell-block wants to merge 2 commits intocashapp:masterfrom
mwentzell-block:fix/symlink-chain-escape-vuln-78098

Conversation

@mwentzell-block
Copy link
Copy Markdown

Summary

PR #540 introduced sanitizeSymlinkTarget() to prevent symlinks from escaping the extraction root during archive extraction. That check uses filepath.Clean, which is purely lexical — it normalizes path strings but does not follow symlinks on disk.

This leaves a chain escape bypass: a tarball can contain multiple symlinks where each individual hop looks safe under filepath.Clean, but the full on-disk resolution escapes the extraction root.

Example chain:

hop1 -> hop2         # filepath.Clean(dest/hop2) = dest/hop2 ✓ passes
hop2 -> ../../escape # filepath.Clean(dest/../../escape) = ../escape ✓ passes

When hop1 is on disk and the OS follows both hops, a subsequent write through hop1 escapes dest.

Fix

After the existing lexical check, resolve the parent directory of destFile using filepath.EvalSymlinks (which follows actual on-disk symlinks) and re-validate the target against the extraction root. If EvalSymlinks fails (parent doesn't exist yet during extraction), the lexical check alone applies — no regression for valid archives.

Testing

Adds TestSymlinkChainEscape to archive_test.go: builds a two-hop tarball in-memory and asserts that extraction fails with an illegal symlink target error and that no file is written outside the destination directory.

Existing TestLinkTraversal and TestLinkTraversalWithStrip tests continue to pass unchanged.


Reported via Block's vulnerability management program (VULN-78098). Bypass of #540.

mwentzell-block and others added 2 commits April 30, 2026 13:26
…itizeSymlinkTarget

PR cashapp#540 introduced sanitizeSymlinkTarget() using filepath.Clean to validate
symlink targets during archive extraction. filepath.Clean is purely lexical and
cannot detect a chain of symlinks where each individual hop appears safe but the
full on-disk resolution escapes the extraction root.

For example, a tarball containing:
  hop1 -> hop2         (lexically within dest — passes the Clean check)
  hop2 -> ../../escape (lexically: dest/hop2/../../escape — also passes)

...allows an attacker to write files outside the destination directory because
the OS follows the full chain at write time.

Fix: after the existing lexical check, resolve the parent directory of destFile
using filepath.EvalSymlinks (which follows actual on-disk symlinks) and re-check
the target against the extraction root. If EvalSymlinks fails (e.g. the parent
doesn't exist yet), the lexical check alone applies.

Adds TestSymlinkChainEscape to archive_test.go covering the two-hop tarball case.
Extract() returns "destination already exists" when dest is pre-created,
causing the test to fail before reaching the symlink validation. The other
traversal tests (TestLinkTraversal, TestLinkTraversalWithStrip) don't
pre-create dest — bring TestSymlinkChainEscape in line with that pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant