Python 3.10 reached end of life on October 1, 2026. The final release, 3.10.22, is a security-only, source-only build, and it carries eight CVEs, three of which are variations on the same weakness in tarfile‘s archive extraction filters. That clustering is the interesting part. It is a bug class, not a batch of unrelated findings, and it explains why the extraction-filter mechanism exists and where its edges are.

If you unpack untrusted archives in Python(CI jobs, upload handlers, plugin installers, data pipelines) the three tarfile CVEs in this release are the ones that matter. One is a filter that was skipped when a link fell back to extracting as an archive member. One defeats path filtering via a hard link pointing at a symlink. One created directories outside the destination for paths that left and returned. Separately, the zipfile decompression-bomb fix is explicitly incomplete for third-party decompressors that monkey-patch the internal hook, and Python says so in the release notes.

Key takeaways:

  • Three of the eight CVEs are the same tarfile extraction-filter weakness. Treat them as one investigation, not three tickets.
  • The hard-link-to-symlink case bypasses naive path checks. If your code validates the extracted path string rather than resolving it, it is affected by construction.
  • The zipfile fix is documented as incomplete for monkey-patched decompressors. If you patch _get_decompressor(), you are still exposed.
  • 3.10.22 is source-only. There are no Windows or macOS installers; 3.10.11 was the last build with binaries.
  • Python 3.11 has roughly a year of security fixes left. That is the realistic migration target, not 3.14.

What shipped

CVEComponentWhat changed
CVE-2026-82049tarfileFilter bypass via hard links to symlinks; could expose files outside the destination and alter permissions and timestamps
CVE-2026-87910tarfileExtraction filters are now applied when a link falls back to extracting an archive member
CVE-2026-19672tarfileFilters no longer create directories outside the destination for paths that leave and return
CVE-2026-15310zipfileDecompression bounded per read for bzip2 and LZMA members
CVE-2026-19553sslwrap_bio() now validates server_side, server_hostname and session; asyncio validates the TLS server_hostname
CVE-2026-19445sslCrash when an SNI callback switches contexts and the original becomes unreferenced
CVE-2026-15806urllib.requestHTTPPasswordMgr credentials are scoped by URL scheme, so HTTPS credentials no longer match HTTP URLs
CVE-2026-17084stringprep / IDNARestricted to RFC 3454 Unicode codepoint attributes
Python 3.10.22, released 1 October 2026. The release also fixes a float/complex formatting crash at precisions near INT_MAX, updates bundled libexpat to 2.8.5, and improves XML hash-flooding protection.

Why three tarfile bugs in one release

Python’s tarfile supports an optional extraction-filter mechanism: a callable you pass to extractall() or extract() that inspects each member and decides whether to allow, restrict or block it. Its whole job is to stop a malicious archive from writing outside the directory you named.

That job turns out to be harder than “check the path starts with my directory,” because a tar archive can express links as well as files and directories. Three of the fixes in this release are three different ways that assumption failed.

Hard links pointing at symlinks

CVE-2026-82049 is the one to understand first, because it defeats the obvious implementation. A filter that checks each member’s path against the destination will look at a hard link whose path is perfectly inside the archive. What it is linked to is the problem: a hard link to a symlink can resolve outside the destination once extraction follows it. The path check passes, and the write escapes.

The practical lesson is that validating a path string is not the same as containing a write. If your filter is written as a string prefix comparison, this class of bug is present whether or not you have upgraded.

The skipped filter

CVE-2026-87910 is a different flavour: not a weak check, but a check that was not run. When a link entry falls back to being extracted as an archive member, the extraction filters are now applied where they previously were not. The gap was a code path where the filter simply never got called.

The leave-and-return path

CVE-2026-19672 concerns archive members whose paths leave the destination and then come back, something like a/../../out/b. The filters no longer create directories outside the destination for such paths. Even when the final write lands somewhere you allowed, creating the intermediate directory tree outside your destination was itself a way to affect state you did not intend to touch.

Three issues, three mechanisms: one weak check, one missing check, one side effect. If you only audit for one of them you will still be wrong after upgrading.

The zipfile fix is deliberately partial

CVE-2026-15310 bounds decompression per read for bzip2 and LZMA members, which is the decompression-bomb defence. The release notes state a limitation that is worth reading twice: third-party decompressors that monkey-patch _get_decompressor() and do not implement needs_input and the two-argument decompress() interface remain vulnerable.

That is Python telling you the fix depends on a private interface staying intact. If your codebase patches _get_decompressor() — a not-uncommon trick for adding formats — you do not get the protection, and no amount of upgrading will change that. Audit for that pattern.

A migration warning in wrap_bio

CVE-2026-19553 has a version-dependent shape that will trip up a naive upgrade. A missing server_hostname with check_hostname enabled emits a DeprecationWarning on 3.10 and 3.11, but raises a ValueError on 3.13 and later. Code that passes today on 3.10 can start throwing on 3.13 with no change of its own.

If you use wrap_bio() with a memory BIO, fix the missing hostname before you move, rather than discovering it as a runtime failure in whichever version you upgrade to next.

What end of life means practically

  • 3.10.22 is source-only. There are no Windows or macOS installers. 3.10.11 was the last release shipped with binaries, so anyone on that version is already building from source.
  • No more fixes are coming. After this release, 3.10 receives nothing, including for actively exploited vulnerabilities.
  • Target 3.11 if you want the smallest jump. Per the release manager’s note, 3.11 has roughly another year of security fixes ahead of it. That is a real runway, not an open-ended commitment.
  • Target 3.14 if you can absorb the change. It is the current feature series. The gap from 3.10 is where the work is.
  • Check your archive-handling code before you upgrade, not after. Find every extractall() and extract() call, and confirm a filter is actually passed.
# Where archive extraction happens in your own code
grep -rn "extractall|.extract(" --include=*.py .

# Third-party code that monkey-patches zipfile internals and stays exposed
grep -rn "_get_decompressor" .

A bare extractall() with no filter is the specific thing to find. The filters exist because unpacking an archive is a write primitive, and an archive is attacker-controlled input if it came from anywhere you did not build.

Related reading on DevStackIO

If your CI or ingestion pipeline is the exposed surface, validating JSON from the command line covers the same class of problem for a different format, and securing AI agents on Linux is relevant when an automated process is the thing unpacking the archive on your behalf.

References

Related Tools

Try these companion tools on DevStackIO Tools: