Guide · Backend
Python's October 2026 Tarfile Vulnerability Explained: Which Filter Is Safe
19 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The Python tarfile vulnerability, public bug ID CVE-2026-114583, lets a crafted tar archive, many files bundled into one, write outside the target folder. It affects code that unpacks with filter='tar'. Python 3.15.2, 3.14.10, 3.13.18, 3.12.16 and 3.11.18 fix it.
- A filter is the check Python runs on every file in an archive before writing it to disk. The 'tar' filter missed a link entry named after the destination folder itself, which could redirect every later file.
- For archives you did not create, use filter='data', the strictest built-in filter. Python 3.14 and later use it when you pass no filter. Python 3.13 and older apply no checks unless you ask.
- To check a machine, run python3 --version, then a short probe script that asks that Python's filters directly. On Linux distribution builds the version number alone can mislead.
- Beginners can stop after the section on what to do today. Engineers then get tested filter results across every supported Python line, why the fix checks names instead of resolved paths, and a partial-extraction trap with a tested helper.
What goes wrong when Python unpacks a tar archive from someone else?
The archive decides where its files go, and a malicious one can try to place them outside the folder you chose. The Python tarfile vulnerability fixed on 25 October 2026 is the latest way through that guard.
A tar archive is one file that holds many files, together with their names, permissions and links. When you unpack it, each stored name becomes a real path on your disk. If a name points somewhere unexpected, a file can land where you never meant it to.
Picture a common setup. A deployment service accepts app bundles as .tar.gz files from many teams' build pipelines and unpacks each one under /srv/releases. The setup is made up, but the code pattern is real and common.
The bundles contain symlinks to absolute paths such as /usr/bin/python3, and Python's strict 'data' filter rejects those. So the team chose filter='tar', which allows absolute links but still promises to keep files inside the destination.
That promise is what CVE-2026-114583 broke. A crafted bundle could slip one special link past the 'tar' filter, and later files in the same bundle could then be written outside /srv/releases. The process's own permissions were the only limit.
Which words do you need before reading the tarfile advisory?
You need seven words. Each comes with a plain meaning and where it shows up in the deployment service.
- Member: one entry inside a tar archive, such as a file, a folder or a link. Each member has a name like app/main.py.
- Destination: the folder you unpack into, here /srv/releases/build-42. Every member should end up somewhere inside it.
- Path traversal: a name that climbs out of the destination, for example ../../etc/cron.d/job, where .. means the parent folder.
- Symlink, short for symbolic link: a small file that points to another path. Opening it opens whatever it points to.
- Hard link: a second name for the same file contents on disk. Changing one name's contents changes the other's.
- Extraction filter: a Python function that inspects each member before it is written, then allows it, changes it or rejects it.
- extractall(): the tarfile method that unpacks every member. Its filter argument picks which extraction filter runs.
What is the Python tarfile vulnerability CVE-2026-114583?
It is a bypass of tarfile.tar_filter, the function behind filter='tar'. The Python Software Foundation's announcement says crafted link entries, such as symlinks with empty or directory-like names, could redirect later members outside the extraction directory.
The announcement rates it Medium. The CVE record gives a CVSS 4.0 score of 6.9, where CVSS is the common 0 to 10 severity scale. It calls the bug the 'tar' counterpart of CVE-2026-7774, the same hole in the 'data' filter, announced on 4 June.
| Release line | Last release without the fix | First release with the fix | Status of the line |
|---|---|---|---|
| 3.15 | 3.15.1 (21 October) | 3.15.2 (25 October) | Bugfix |
| 3.14 | 3.14.9 (21 October) | 3.14.10 (25 October) | Bugfix |
| 3.13 | 3.13.17 (21 October) | 3.13.18 (25 October) | Security fixes only |
| 3.12 | 3.12.15 (30 September) | 3.12.16 (25 October) | Security fixes only |
| 3.11 | 3.11.17 (1 October) | 3.11.18 (25 October) | Security fixes only |
| 3.10 | 3.10.22 (1 October), final | None | End of life |
As of 28 October, the CVE record names the same five first fixed releases as the release pages, and our probe confirmed the fix in each.
Do not read 3.14.10 and 3.15.2 as tarfile releases. Their release pages call them expedited security releases for compromised eMudhra certificate authority keys, keys that vouch for websites' identities. The tarfile fix is one of several fixes that shipped alongside.
How do Python's three extraction filters differ?
'fully_trusted' checks nothing, 'tar' blocks the most surprising tricks but keeps Unix features, and 'data' blocks almost everything a plain data bundle should not need. The table shows what each filter did with sample members.
Two words in the table need a meaning first. Setuid is a permission bit that makes a program run as its file's owner, often root. A device file is a special file that talks to hardware. Both are normal on a Unix system disk and strange inside a downloaded bundle.
| Member in the archive | fully_trusted | tar | data |
|---|---|---|---|
| Regular file a.txt | Allowed | Allowed | Allowed |
| File named ../evil.txt | Allowed | Rejected | Rejected |
| File named /abs.txt | Allowed | Renamed to abs.txt | Renamed to abs.txt |
| File with setuid mode 4755 | Allowed | Mode cut to 755 | Mode cut to 755 |
| Character device file | Allowed | Allowed | Rejected |
| Symlink s pointing to /etc | Allowed | Allowed | Rejected |
| Symlink s pointing to ../outside | Allowed | Allowed | Rejected |
| Symlink named . (the destination) | Allowed | Rejected since this fix | Rejected since the CVE-2026-7774 fix |
Think of the filters as a guard at a building's loading bay. The building is the destination, and each member is a parcel with a delivery label. 'fully_trusted' waves every parcel in.
'tar' reads each label and turns away parcels addressed outside the building. It still accepts signposts, the symlinks, even ones pointing to other buildings. 'data' also refuses any signpost that points outside and any odd machinery, the device files.
Where the picture breaks: a real guard sees a parcel once. tarfile checks each member against the folder as it exists at that moment, so earlier members can change what later checks see. That is exactly the gap this CVE used.
How did a link named after the destination slip past the tar filter?
The 'tar' filter asked whether a member's path, after following links, stays inside the destination. A link whose name means the destination itself, such as a single dot, stays inside by that test, so it passed.
The filter works out the destination by resolving its real path afresh for each member. A comment in the patched Lib/tarfile.py states the danger. Such a link would replace the destination with a link, redirecting the destination for all subsequent members.
The 'tar' filter never inspects where a link points. That is by design, because 'tar' must allow absolute links like the deployment service's /usr/bin/python3. Its inside-the-folder test was the only guard, and this link bent the folder itself.
How the tar filter judged one member before and after the fix
Drag to exploreFollow the two arrows out of the name check. Before 3.14.10 and its sister releases, a link named after the destination took the left path and was allowed.
Diagram connections:
- Next archive member to Strip leading slashes
- Strip leading slashes to Resolves inside folder?
- Resolves inside folder? to Reject the member (No)
- Resolves inside folder? to Link named the folder? (Yes)
- Link named the folder? to Old filter: allowed (Yes, before the fix)
- Old filter: allowed to Later members follow it
- Link named the folder? to Write the member (No)
- Next archive member: Name, type and link target read from the archive
- Strip leading slashes: /abs.txt becomes abs.txt
- Resolves inside folder?: Real path checked against the destination
- Reject the member: OutsideDestinationError, extraction stops
- Link named the folder?: New check: is the normalised name just a dot?
- Old filter: allowed: Link could replace the destination itself
- Later members follow it: Their inside check now measures from the new place
- Write the member: Mode bits trimmed, then written to disk
After the fix, the Yes arrow from the new check goes to the reject box instead. We confirmed the decision change on real builds by calling the filters on in-memory members. We did not build an archive that writes outside a folder.
What should you do today if your code extracts tar files?
Find every place you unpack archives, upgrade Python to a fixed release, and pass filter='data' for anything you did not build yourself. Then prove the running Python has the fix.
Find the calls
Search your code with grep -rnE "extractall|\.extract\(|unpack_archive" --include=*.py . and note which filter each call passes. shutil.unpack_archive uses tarfile underneath for tar files.
Check the Python that runs
Run python3 --version inside the real container or server, not on your laptop. A fixed build prints 3.15.2, 3.14.10, 3.13.18, 3.12.16 or 3.11.18, or a later release in the same line. Note that 3.15.0 and 3.15.1 are not fixed.
Run the probe
Run the script below with that same Python. It asks both filters about one harmless in-memory member and prints whether each rejects it.
Upgrade
Rebuild images on a fixed base image, or install the fixed release. Restart long-running workers, because a running process keeps the old tarfile code in memory.
Pick the filter on purpose
Use filter='data' for uploads, downloads and anything from outside your team. Keep 'tar' only for archives you build and sign yourself.
Unpack into a fresh folder
Extract into a new temporary folder, and delete it if extraction fails. The helper later in this guide does both.
# check_tar_fix.py: run it with the same Python your app uses
import sys, tarfile, tempfile
print("Python", sys.version.split()[0])
if not hasattr(tarfile, "data_filter"):
sys.exit("No extraction filters at all: upgrade this Python")
probe = tarfile.TarInfo(".") # a link named after the destination
probe.type = tarfile.SYMTYPE
probe.linkname = "sub"
with tempfile.TemporaryDirectory() as dest:
for name, check in [("data", tarfile.data_filter),
("tar", tarfile.tar_filter)]:
try:
check(probe, dest) # only asks the filter; writes nothing
print(f"filter='{name}': accepts the probe (unpatched)")
except tarfile.FilterError:
print(f"filter='{name}': rejects the probe (patched)")On 3.14.9 it printed rejects for 'data' and accepts the probe (unpatched) for 'tar'. On 3.14.10 both lines said rejects the probe (patched). The data line said patched on every build we tested, because the earlier CVE-2026-7774 fix covers that filter.
Why the version number is not enough on Linux
Distributions such as Ubuntu patch Python themselves and often keep the old upstream number. Ubuntu 24.04's python3.12 package 3.12.3-1ubuntu0.17 reports 3.12.3, which looks ancient.
On 28 October our probe found that this build rejects the 'data' probe but still accepts the 'tar' probe. So it behaves as if it has the CVE-2026-7774 fix but not the October one. Trust the probe over the number, and watch your distribution's tracker.
Windows and macOS users on Python 3.13
The 3.13.18 release page says binary installers are no longer provided for 3.13, and 3.13.17 was the last release with them. If you use the python.org installer, move to 3.14.10 or 3.15.2, or build 3.13.18 from source.
When is filter='tar' still the right choice?
Use 'tar' only for archives you built or verified yourself that need Unix features 'data' blocks. Examples are absolute symlinks, links that point outside the bundle, device files or stored owners.
Backups of a whole system that your own team made fit that description. A user upload or a file fetched from a URL does not. Even when you trust the source, the deployment service is better off checking a signature first and then using 'tar'.
If your bundles only need absolute symlinks, consider changing the build to use relative links. In our tests, 'data' accepted a relative symlink to a path inside the bundle and rejected every absolute symlink we tried.
Which extraction filter does each Python version use by default in 2026?
Python 3.14 and later use 'data' when you pass no filter. Python 3.12 and 3.13 use 'fully_trusted' and emit a DeprecationWarning, a printed notice that the behaviour will change. Python 3.11 and 3.10 use 'fully_trusted' silently.
We tested this on 28 October with one benign archive holding a setuid file and an absolute symlink. We extracted it into a temp folder with no filter argument on each fixed release.
| Python | Filter used | Setuid file kept 4755? | Absolute symlink created? | Warning shown |
|---|---|---|---|---|
| 3.10.22, 3.11.18 | fully_trusted | Yes | Yes | None |
| 3.12.16, 3.13.18 | fully_trusted | Yes | Yes | DeprecationWarning |
| 3.14.10, 3.15.2 | data | No, cut to 755 | No, AbsoluteLinkError | None |
Python shows a DeprecationWarning by default only when code in your __main__ script triggers it. In our 3.13.18 test, the same call inside an imported module printed nothing.
PEP 706, the design document that added filters in Python 3.12, planned this exact sequence. It also said security backports to older lines should default to 'fully_trusted' without a warning, which matches what 3.11 did in our test.
The tarfile documentation recommends checking hasattr(tarfile, 'data_filter') rather than the version number to see whether filters exist. Every build we tested, back to 3.10.22, had them.
Why did the fix switch from a resolved-path check to a name check?
The new check looks only at the member's name, which no earlier member can change. The old one compared resolved real paths, which earlier members can change. The diff between 3.14.9 and 3.14.10 shows the move.
Before the fix, a check lived only inside the 'data' branch of the shared helper _get_filtered_attrs. It rejected a link when the member's own resolved path equalled the resolved destination. 'tar' skipped that branch entirely.
The fix deletes that check and adds one in the part both filters share. It rejects any hard link or symlink whose normalised name is a single dot. Normalised means os.path.normpath has removed extra slashes, single dots and .. steps.
That catches the variants we tried: a name of '.', an empty name, './' and 'd/..'. All four were rejected by 'tar' on 3.14.10 and accepted on 3.14.9. A directory member named '.' is still allowed, because the new check covers only hard links and symlinks.
The name check also removed a false alarm
The fix author noted on the pull request that the old check was wrong in the other direction too. We reproduced it with a destination that already held a symlink x pointing to the folder itself.
A new link member named x would only replace that symlink. On 3.14.9 and 3.13.17, 'data' rejected it with OutsideDestinationError. On 3.14.10 and 3.13.18 it was accepted, because its name is x, not a dot.
What does a safer tar extraction helper look like in Python?
Unpack into a brand-new temporary folder with filter='data', delete it if anything fails, and move it into place only on success. The sample below ran unchanged on 3.10.22, 3.11.18, 3.12.16, 3.13.18, 3.14.10 and 3.15.2.
import os, shutil, tarfile, tempfile
def safe_extract(archive_path, final_dir):
"""Unpack into a fresh temp folder with the data filter, then move it."""
parent = os.path.dirname(os.path.abspath(final_dir))
work = tempfile.mkdtemp(dir=parent) # new, empty, random name
try:
with tarfile.open(archive_path) as tf:
tf.extractall(work, filter="data")
os.rename(work, final_dir) # refuses a non-empty final_dir
except Exception:
shutil.rmtree(work) # drop the unfinished tree
raiseA good bundle arrived complete in its final folder. A bundle with an absolute symlink raised AbsoluteLinkError and left no folder behind. Running it a second time into the same, now full, folder raised OSError instead of mixing two releases, and again left no temporary folder behind.
The partial-extraction trap
A filter error stops extractall() part-way, and files written before the bad member stay on disk. In our default-filter test on 3.14.10, the setuid file was already written when the symlink after it failed.
Without cleanup, a half-unpacked release can sit in /srv/releases and get picked up later. The helper's rmtree removes it. tempfile.mkdtemp also creates the folder with mode 700, readable only by its owner, so loosen permissions after the move if other users need access.
A fresh, randomly named folder helps in one more way. The announcement for CVE-2026-19672 says extraction into a randomly named directory is not affected by that bug. The tarfile documentation also suggests a new temporary folder to avoid pre-existing links.
The filters do not limit size. The documentation says they do not prevent denial-of-service attacks, so cap file count, total bytes and name length yourself, and set disk and memory limits outside Python.
Why does Python's tarfile keep getting extraction CVEs in 2026?
Because tar is an old format full of links, permissions and repeated names, and the filters are a list of known bad patterns. Each 2026 bug found a new combination the list missed.
| CVE | Announced | Severity | What it allowed |
|---|---|---|---|
| CVE-2026-7774 | 4 June | Medium | 'data' filter bypass with oddly named links that redirect later members |
| CVE-2026-19672 | 19 August | Medium | Empty folders created outside the destination, on POSIX systems only |
| CVE-2026-87910 | 11 September | Medium | A custom filter's rejection ignored on systems without link support |
| CVE-2026-82049 | 14 September | High | A hard link to a symlink changing or exposing files outside the destination |
| CVE-2026-114583 | 22 October | Medium | The CVE-2026-7774 trick, now against the 'tar' filter |
The documentation is frank about the limit. Its extraction filter section warns that none of the filters blocks all dangerous archive features, and that untrusted archives need inspection first.
For the deployment service, the durable answer is layers: a signature check, the 'data' filter, a fresh folder, size caps and an unprivileged user. If one layer misses the next trick, the others still stand.
Which tarfile filter should you pass?
Drag to exploreStart at the top. Notice that only one path, archives you built and verified yourself, leads to 'tar', and none leads to 'fully_trusted' for outside files.
Diagram connections:
- Archive to unpack to Did you build it?
- Did you build it? to filter='data' (No or not sure)
- Did you build it? to Needs Unix extras? (Yes)
- Needs Unix extras? to filter='data' (No)
- Needs Unix extras? to filter='tar' (Yes)
- filter='data' to Fresh temp folder
- filter='tar' to Fresh temp folder
- Archive to unpack: Upload, download, build output or backup
- Did you build it?: Your pipeline, verified by signature or checksum
- Needs Unix extras?: Absolute links, device files or stored owners
- filter='data': Default on 3.14 and later; pass it on older lines
- filter='tar': Needs 3.14.10, 3.13.18, 3.12.16, 3.11.18 or 3.15.2
- Fresh temp folder: Delete on error, move into place on success
How do you explain the Python tarfile vulnerability in a system design interview?
Treat it as a question about untrusted input that turns into file paths, and about guards that depend on state the attacker can change. Use this frame.
Name the boundary
Archive member names come from outside, and extraction turns them into paths on your disk. Everything after that line must assume hostile input.
Pick the strictest filter that works
Use 'data' for anything external. Explain that 'tar' allows absolute links by design, so it relies on a single path check.
Explain the check-versus-state trap
Earlier members can change the folder that later checks measure against. Prefer checks on the input itself, such as the name, over checks on disk state.
Add layers
Verify signatures, unpack into a fresh folder as an unprivileged user, cap size and file count, and delete partial results on error.
Show how you know it is patched
Run a behaviour probe inside each image, not just a version check, because distribution builds keep old version numbers.
Interviewers listen for the step from 'we upgraded Python' to 'we proved the running interpreter rejects the bad input, and we limited the damage if it did not'.
Questions people ask
- Am I affected if I only use filter='data'?
- Not by CVE-2026-114583, which is a 'tar' filter bug. The same trick against 'data' was CVE-2026-7774, fixed in 3.14.6, 3.13.14, 3.12.14, 3.11.16 and 3.10.21. Other 2026 tarfile CVEs also touched 'data', so upgrading to a current release is still the right move.
- Am I affected if I call extractall() with no filter?
- On Python 3.13 and older, a call with no filter uses 'fully_trusted', which performs none of these checks at all. That is worse than this CVE. Pass filter='data' explicitly. On 3.14 and later the default is already 'data', so this 'tar' bug does not apply to calls without a filter.
- Does upgrading Python fix my Docker images automatically?
- No. An image keeps the Python it was built with. Rebuild on a base image that ships a fixed release, redeploy and restart workers. Then run the probe script inside the running container to confirm, since the image tag alone does not prove what is inside.
- Is there a fix for Python 3.10?
- No. Python 3.10 reached end of life on 1 October 2026 with 3.10.22, and our probe showed its 'tar' filter still accepts the link. Its 'data' filter has the CVE-2026-7774 fix, added in 3.10.21. If you cannot upgrade yet, use filter='data' and plan a move to 3.14 or 3.15.
- Is zipfile affected by this bug?
- This CVE covers tarfile only, according to its CVE record and announcement. zipfile has no extraction filter argument and handles links differently. Treat untrusted zip files with the same care: a fresh folder, size and count limits, and checks on every name before writing.
- How do I check the Python version inside a running container?
- Run docker exec followed by the container name and python3 --version. Better, copy check_tar_fix.py into the container and run it there with the same Python the app uses. The probe tests behaviour, so it also works for distribution builds whose version numbers lag.
Sources and further reading
- Python security-announce: CVE-2026-114583 tarfile.tar_filter path traversal bypass (22 October 2026)
- CVE record API: CVE-2026-114583 (affected ranges, CVSS 6.9)
- CPython pull request 160098: Reject links replacing the destination in tarfile.tar_filter
- CPython fix commit e7e0b35 (main branch, with NEWS entry and test change)
- Python 3.14.10 release page (expedited eMudhra release; lists the tarfile fix)
- Python 3.13.18 release page (no binary installers for 3.13)
- Python 3.12.16 release page
- Python 3.11.18 release page
- Python 3.15.2 release page
- python.org downloads: release dates and support status
- Python documentation: tarfile extraction filters
- PEP 706: Filter for tarfile.extractall
- Python security-announce: CVE-2026-7774 tarfile.data_filter bypass (4 June 2026)
- CVE record API: CVE-2026-7774 (fixed versions)
- Python security-announce: CVE-2026-19672 (19 August 2026)
- Python security-announce: CVE-2026-87910 (11 September 2026)
- Python security-announce: CVE-2026-82049 (14 September 2026)