Conversation
patool has no central sanitization of '..' relative path components (or absolute paths) in TAR/ZIP archive member names. The tempdir used for extraction is created in the current working directory, so a single '../' in a member name already reaches the CWD, and further '../' components climb into ancestor directories. Not every backend archive program guards against this on its own (eg. GNU tar on Linux does not by default), so a malicious archive could write files outside the requested extraction directory. Add util.get_unsafe_archive_member(), which lists a TAR or ZIP archive's members with Python's own tarfile/zipfile modules (without extracting) and checks whether any member would resolve outside of the extraction directory. _extract_archive() now calls this before dispatching to any extraction backend (external program or native module) and refuses to extract at all if an unsafe member is found. This intentionally covers only TAR and ZIP, since those are the two formats patool can list uniformly via the Python standard library regardless of which extraction backend ends up being used. Other archive formats are unchanged and still rely solely on their backend program's own protections, as documented in SECURITY.md. If the archive can't be listed at all (eg. a .tar.Z or pre-3.14 .tar.zst that the stdlib tarfile module can't open, though an external tar command can), the check is skipped and extraction proceeds as before - this fix must not regress support for compression variants the stdlib doesn't handle itself. Fixes wummel#193
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Following up on the maintainer's suggested approach in the issue ("list all members of an archive before extraction and inspect them" / "extract only inspected and safe members"), this implements a central guard for the two formats patool can inspect uniformly regardless of backend: TAR and ZIP.
patoolib.util.get_unsafe_archive_member(format, archive, outdir)lists a TAR/ZIP archive's members with Python's owntarfile/zipfilemodules (list-only, no extraction) and checks whether any member's path would resolve outside ofoutdir(via..components or an absolute path)._extract_archive()calls this before dispatching to any extraction backend — external program or native module — and raisesPatoolError(reject, not strip-and-continue) if an unsafe member is found, so nothing gets written at all rather than a partially-sanitized extraction.Why TAR/ZIP only, for now: those are the only two formats where patool can list members with a single, uniform stdlib API regardless of which actual backend the user has installed (
7z, GNU tar,star,bsdtar,unzip,unar,jar, ...). The other ~13 formats each have their own listing quirks per external program, so a uniform guard for all of them is a separate, larger effort I didn't want to bundle into one PR. They're unchanged and still rely solely on their backend's own protections, exactly as documented inSECURITY.mdtoday.Fail-open on unlistable archives: if the stdlib module can't even open the archive (eg. a
.tar.Z, or a.tar.zston Python < 3.14, both of which an externaltarbinary handles fine but the stdlibtarfilemodule can't), the check is skipped and extraction proceeds as before. I want to flag this explicitly for review: I chose compatibility over "fail closed" here, since breaking working extraction for a compression variant is a real regression a security check shouldn't introduce as a side effect. Happy to discuss if a stricter default is preferred.Fixes #193
Testing
New file
tests/test_extract_security.py:../relative traversal and absolute-path variants) with a crafted member name, assertspatoolib.extract_archive()raisesPatoolErrorand nothing is written outside the output directory. Also asserts a normal (safe) archive still extracts correctly (regression guard).util.get_unsafe_archive_member()directly for both formats, plus confirms an unchecked format (7z) returnsNone.Ran locally (Python 3.12, since
pyproject.tomlrequires>=3.12and my system default was 3.11):pytest tests/— 110 passed, 132 skipped (skipped tests need external programs not installed here, eg.7z/unrar). 3 pre-existing failures unrelated to this change and reproducible on unmodifiedmain(missing Java runtime forjar; systemziplacking password support) — confirmed viagit stashbefore attributing them.ruff check/ruff format --check(pinned0.16.5) — clean.ty check(pinned0.0.75) — same 2 pre-existing diagnostics as unmodifiedmain(missing optionalargcomplete/setuptoolsin my minimal venv), confirmed viagit stash -u; no new diagnostics from this change.I also manually verified the underlying vulnerability class against the real system
tar/unzipbinaries directly (not through patool) to understand what's actually exploitable: on this machine (macOS, bsdtar + Info-ZIP unzip) both already refuse/strip../on their own, which is why the fix here is backend-independent rather than relying on any specific tool's protections — GNU tar on Linux (patool's most common deployment target) does not have the same built-in guard.Notes
akshar27/patool) — a one-time manual step outside this PR. I'll push a retrigger commit once it's on, if CI doesn't pick this PR up.