Repository navigation
tarfile filter="data" behavior slightly loosened after gh-160097 #160863
Description
Bug report
gh-160097 gave tar_filter a check that data_filter already had. In moving the check, the condition changed. It was "the member's resolved path equals the destination". It is now "the member's normalised name is ."
A symlink or hard-link member can still reach the destination, and now passes filter="data", in three ways:
../dest;s/.after an earlier members -> ., which needs no knowledge of the destination;../E, whereEis an existing symlink outside the destination that points at it.
vs before:
extract()andextractall()may raiseRecursionErrorwhere they raisedOutsideDestinationError.- The extraction completes and changes the destination directory's own permissions.
- A symlink outside the destination that points at it is deleted or replaced. That includes the symlink through which the caller named the destination.
reproducer
common code used be the scenarios below:
import io, os, tarfile, tempfile
def archive(*members): # (name, type, linkname, mode, data)
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
for name, type, linkname, mode, data in members:
info = tarfile.TarInfo(name)
info.type, info.linkname, info.mode, info.size = type, linkname, mode, len(data)
tf.addfile(info, io.BytesIO(data))
buf.seek(0)
return tarfile.open(fileobj=buf)- RecursionError vs prior OutsideDestinationError
with tempfile.TemporaryDirectory() as box:
os.mkdir(box + "/dest")
with archive(("../dest", tarfile.SYMTYPE, "dest", 0o777, b"")) as tf:
tf.extractall(box + "/dest", filter="data")- link destination permissions
with tempfile.TemporaryDirectory() as box:
os.mkdir(box + "/dest", 0o700)
with archive(("d", tarfile.DIRTYPE, "", 0o755, b""),
("s", tarfile.SYMTYPE, ".", 0o777, b""),
("s/.", tarfile.LNKTYPE, "d", 0o777, b"")) as tf:
tf.extractall(box + "/dest", filter="data")
print("destination mode:", oct(os.stat(box + "/dest").st_mode & 0o7777))that was previously an OutsideDestinationError, now it changes the mode of the destination. security regression, this one lets any destination path be targeted.
- symlink outside destination
with tempfile.TemporaryDirectory() as box:
os.mkdir(box + "/real")
os.symlink("real", box + "/current")
with archive(("first.txt", tarfile.REGTYPE, "", 0o644, b"1"),
("../current", tarfile.SYMTYPE, "real", 0o777, b""),
("later.txt", tarfile.REGTYPE, "", 0o644, b"2")) as tf:
tf.extractall(box + "/current", filter="data")
print("current is a symlink:", os.path.islink(box + "/current"))
print("current/:", os.listdir(box + "/current"), " real/:", os.listdir(box + "/real"))That should've been an OutsideDestinationError, now it mucks with things outside... security regression, but requires a pre-existing symlink to target.
Fix thoughts
Minimal: put the five removed lines in tarfile from https://patch-diff.githubusercontent.com/raw/python/cpython/pull/160098.patch back while keeping the six new ones?
Broader: judge a link member by where the link is made (ie: did the tarfile extraction write it), resolving the parent directory only.
CPython versions tested on:
3.13, CPython main branch
Operating systems tested on:
Linux
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsNo status
Activity
gpshead commentedon Oct 29, 2026
the minimal regression fix should look something like https://github.com/gpshead/cpython/pull/24/files
the broader fix might look something like https://github.com/gpshead/cpython/pull/23/files
StanFromIreland commentedon Oct 29, 2026
Good catch,
tarfileis just the gift that keeps on giving ;-)That looks good.