Skip to content

reject symlink targets that escape the extraction directory#403

Merged
kuba-- merged 3 commits into
kuba--:masterfrom
jmestwa-coder:symlink-target-containment
Jun 2, 2026
Merged

reject symlink targets that escape the extraction directory#403
kuba-- merged 3 commits into
kuba--:masterfrom
jmestwa-coder:symlink-target-containment

Conversation

@jmestwa-coder

Copy link
Copy Markdown
Contributor

A symlink entry's target is written verbatim, so an archive carrying an absolute or ../-climbing target plus a second entry of the same name writes through the link to a path outside the destination. zip_archive_extract now walks the target against the symlink's own depth and refuses to create one that resolves above the extraction root. In-tree relative links still extract.

@kuba--

kuba-- commented Jun 1, 2026

Copy link
Copy Markdown
Owner

A symlink entry's target is written verbatim, so an archive carrying an absolute or ../-climbing target plus a second entry of the same name writes through the link to a path outside the destination. zip_archive_extract now walks the target against the symlink's own depth and refuses to create one that resolves above the extraction root. In-tree relative links still extract.

Thanks, could you add some tests to test/test_extract.c, e.g.:

  • a → . (symlink, allowed: depth=0, target ".")
  • a/x → .. (symlink, allowed: depth=1, decremented to 0)
  • a/x/foo (regular file)
  • absolute target (/etc/passwd) → rejected with ZIP_EINVENTNAME,
  • ../escape at archive root → rejected,
  • in‑tree ../sibling from dir/link → still extracts,
  • benign link → file → still extracts.

@jmestwa-coder

Copy link
Copy Markdown
Contributor Author

Added them in test/test_extract.c. One archive covers the allowed cases together (a → ".", a2/x → "..", a regular file under a dir, an in-tree d/link → "../sibling", and benign link → file.txt), each verified with readlink/content. The two escapes (absolute /etc/passwd and ../ at root) get their own archives and assert ZIP_EINVENTNAME.

I kept the depth cases as standalone entries rather than chaining a/x/foo through the symlink, since extracting a regular file through an allowed link is the escape we're actually blocking and I didn't want the test to write outside its temp dir. Since there's no public API to author a symlink entry, the test builds the stored zip bytes directly and flips the Unix made-by byte + S_IFLNK attr.

@kuba--

kuba-- commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Added them in test/test_extract.c. One archive covers the allowed cases together (a → ".", a2/x → "..", a regular file under a dir, an in-tree d/link → "../sibling", and benign link → file.txt), each verified with readlink/content. The two escapes (absolute /etc/passwd and ../ at root) get their own archives and assert ZIP_EINVENTNAME.

I kept the depth cases as standalone entries rather than chaining a/x/foo through the symlink, since extracting a regular file through an allowed link is the escape we're actually blocking and I didn't want the test to write outside its temp dir. Since there's no public API to author a symlink entry, the test builds the stored zip bytes directly and flips the Unix made-by byte + S_IFLNK attr.

Thanks, could you fix formating (clang-format action), just check https://github.com/kuba--/zip/blob/master/CONTRIBUTING.md

@jmestwa-coder

Copy link
Copy Markdown
Contributor Author

Done, ran clang-format --style=LLVM over the changed files and pushed. It only touched the comment alignment in the test's entry table; tests still pass.

@kuba-- kuba-- merged commit fdabd29 into kuba--:master Jun 2, 2026
18 checks passed
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.

2 participants