Programming · free software · the open webSubscribe via RSS

Python’s tar filter shows what a security boundary must cover

Python 3.15.2 fixed a path escape in tarfile’s explicit tar filter. The flaw shows why extraction safety depends on the order of archive entries, while the release’s separate TLS fix illustrates a very different kind of trust decision.

A filter can inspect one member and still miss a changing destination

Python 3.15.2, released on 25 October 2026, included an expedited security update for a flaw in tarfile’s tar extraction filter. A crafted archive could use a link entry whose name resolved to the destination directory itself; later entries could then be redirected outside that directory. The fix rejects links that would replace the extraction destination. The project’s change was merged on 22 October and backported across maintained branches before the release. [1] [2]

The interesting boundary is temporal. An archive is not just a bag of independent filenames. Extraction processes entries in sequence, and earlier entries can change how later pathnames resolve. A check that asks whether a member name looks contained can miss a link that changes the meaning of the next name. This is the same reason pathname safety cannot generally be reduced to searching a string for ..: containment is a property of resolution in context, not of a spelling considered alone. [3]

Choose a filter for the archive’s purpose

Since Python 3.14, the default extraction filter is data, which blocks more filesystem-specific features and rejects links that escape the destination. The tar filter is an explicit choice for applications that need more of the archive’s Unix semantics. Choosing a more restrictive policy narrows what the archive can express; it does not prove the archive is benign. A downstream application that opts into tar has a different contract from one using the default, so release notes and call sites need to be read together. [3]

For a data import, an application can select the policy directly instead of silently relying on a global or version-dependent default. The short wrapper below checks that extraction filters exist, then passes the data filter explicitly. It is only the core call: destination should be fresh and controlled, and the caller still needs resource limits, inspection and cleanup. Extraction filters were introduced in Python 3.12 and may be backported by distributors. [3]

import tarfile

def extract_data_archive(archive_path, destination):
    data_filter = getattr(tarfile, "data_filter", None)
    if data_filter is None:
        raise RuntimeError("Python extraction filters are required")

    with tarfile.open(archive_path, "r:*") as archive:
        archive.extractall(path=destination, filter=data_filter)
Python 3.12+ or a distribution with extraction-filter backports; use a new destination and apply resource limits.

The fix closes one gap, not every archive risk

The vulnerable call selected filter="tar", which preserves more Unix-style metadata and link behavior than the cross-platform data filter. A crafted link entry whose name resolves to the destination itself could redirect later writes. The project’s patch rejects a link member when its normalized member name would replace the extraction destination. It does not make every archive operation safe, and it should not be described as a flaw in every extractall() call. [1] [2]

The security announcement describes crafted link entries, including symbolic links with empty or directory-like names. The deeper issue is the archive’s sequence: an earlier member can affect how a later name resolves. A check that asks whether each spelling looks contained can miss that changing context. This is why path safety cannot generally be reduced to searching a string for ..; containment is a property of resolution in context, not of a name considered alone. [2]

Even with the data filter, a malicious archive can consume too much disk, CPU or memory, and an extraction error may leave a partial tree. Extract into a new temporary directory and publish its contents only after validation succeeds. Cap member count, expanded bytes, individual file size and time outside the filter. If links are unnecessary, rejecting them altogether reduces the state the extractor must reason about. [3]

The same release repaired a different kind of trust

The release also blocked compromised eMudhra intermediate certificate keys in Python’s ssl module. That is a different boundary: TLS validation asks whether a peer’s certificate chains to keys the client trusts, while archive extraction asks where a sequence of filesystem operations may write. The Python release page describes its CA-key block as a last resort while operating-system or bundled trust stores are updated. In ordinary clients, ssl.create_default_context() remains the documented starting point because it enables certificate validation and hostname checks. [1] [4]

These fixes suggest two practical review questions. For archives, identify the exact extraction filter and ask how earlier members affect later resolution. For TLS, identify the trust store actually loaded by the runtime and how that store receives updates. A patch version can carry fixes for both, but upgrading only helps a process that is running that patched interpreter; container images, embedded Python copies and application-bundled certificates can each have their own update path. The maintenance release is therefore a useful reminder to inventory runtime boundaries, not a substitute for doing so.

References

  1. Python 3.15.2 release notes
  2. CPython: reject links replacing the tarfile extraction destination
  3. Python 3.15 tarfile documentation: extraction filters
  4. Python 3.15 ssl documentation: security considerations

Suggest a correction · All posts