Repository navigation
Description
Description of the issue
I'm wondering whether the argument positions used by the py/tarslip sinks are intended, because they don't seem to match TarFile.extract(member, path="", set_attrs=True, *, numeric_owner=False, filter=None) and TarFile.extractall(path=".", members=None, *, numeric_owner=False, filter=None). As a result, tar.extract(member=entry, path=dest) is not reported, although tar.extract(entry, path=dest) is, and tar.extractall(dest, safe_members(tar)) is reported even when safe_members filters the members, although tar.extractall(dest, members=safe_members(tar)) is not.
Affected sinks
ExtractSinkonly usescall.getArg(0), so a member passed asmember=...is never a sink.ExtractAllSinkreports the receiver whennot exists(call.getParameter(2, "members")). As far as I can tell,API::CallNode.getParameter(i)isgetArg(i), which does not countself, so a positionalmembersis argument 1 and argument 2 never exists in a valid call. A positionalmembersis therefore treated as if there were none.ExtractMembersSinkusescall.getArg(0), which forextractallispath, notmembers.hasUnsafeFilterusesgetParameter(4, "filter"). Ifselfis counted, 2 and 4 are exactly the positions ofmembersandfilterinextractall(self, path, members, *, numeric_owner, filter). Forfilterthis is harmless because it is keyword-only.
The members index came with #15051. The query tests only pass members= by keyword and the member to extract positionally, so they don't cover these forms.
Steps to reproduce
Not reported (false negative):
import sys
import tarfile
with tarfile.open(sys.argv[1]) as tar:
for entry in tar:
tar.extract(member=entry, path="/tmp/unpack/")Reported, for comparison: the same file with tar.extract(entry, path="/tmp/unpack/"). Adding filter="fully_trusted" to the keyword form is also not reported, while the positional form with that filter is reported (as in the query test tar.extract(entry, filter=extraction_filter)).
Reported (false positive), at the receiver tar in the last line:
import os
import sys
import tarfile
def safe_members(tar):
for info in tar:
if info.issym() or info.islnk():
continue
if os.path.isabs(info.name) or ".." in info.name:
continue
yield info
with tarfile.open(sys.argv[1]) as tar:
tar.extractall("/tmp/unpack/", safe_members(tar))Not reported, for comparison: the same file with members=safe_members(tar). python-security-extended.qls gives the same results as py/tarslip alone.
I ran each file unchanged except for the destination directory. The extract files got an archive with ok.txt and ../dotdot.txt. The extractall files got an archive that also contains a symbolic link link → .. followed by link/pwned.txt, and a hard link hl → ../victim.txt followed by a regular member hl.
| Python | extract(entry, ...) (reported) |
extract(member=entry, ...) (not reported) |
extract(member=entry, ..., filter="fully_trusted") (not reported) |
extractall(dest, safe_members(tar)) (reported) |
|---|---|---|---|---|
| 3.10.12 | ../dotdot.txt written |
../dotdot.txt written |
../dotdot.txt written |
nothing written outside dest |
| 3.12.3 | ../dotdot.txt written |
../dotdot.txt written |
../dotdot.txt written |
nothing written outside dest |
| 3.13.16 | ../dotdot.txt written |
../dotdot.txt written |
../dotdot.txt written |
nothing written outside dest |
| 3.14.8 | OutsideDestinationError |
OutsideDestinationError |
../dotdot.txt written |
nothing written outside dest |
So the keyword form behaves exactly like the reported positional form, and the filtered members passed positionally behaves exactly like the unreported keyword form (nothing is written outside dest either way). The keyword form is not unusual: a GitHub code search for "extract(member=" language:python tarfile currently finds about 390 files.
Question
Are these argument positions intended? Would it be reasonable to use getParameter(0, "member") in ExtractSink, and position 1 for members in both ExtractAllSink and ExtractMembersSink (so that an unfiltered positional members stays reported once the receiver is no longer)?
Environment
- CodeQL CLI 2.27.4 (
codeql/python-queries1.9.0); CodeQL CLI 2.27.2 and 2.15.4 give the same results - CodeQL repository commit for the links above:
fbf9fca5ce7c3b64c2f5d719ea0444577850cb4a(TarSlipCustomizations.qllandApiGraphs.qllare identical in 2.27.4 and this commit) - Python 3.10.12, 3.12.3 (Ubuntu), 3.13.16 and 3.14.8 on Linux/amd64
Activity
sam-robson commentedon Nov 12, 2026
Thanks for the report @wildoranges. I've opened a PR with the fix.