-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Description
ProcessWire’s archive handling extracts user-supplied ZIP files before any validation and without resource limits. Specifically, WireUpload::saveUploadZip()
writes uploaded archives to a per-request temp directory (.zip_tmp
) and immediately invokes WireFileTools::unzip($zipFile, $dst)
. The unzip()
routine iterates all ZIP entries and calls ZipArchive::extractTo()
for each, applying only a simple ..
substring guard and no checks on total uncompressed size, number of entries, directory depth, or extraction time. Filtering by extension/size and any cleanup occur only after extraction. This behavior is reachable from core features that enable ZIP uploads, such as the Language Support file fields (created with unzip=1
, accessible to a user with the lang-edit
permission) and the Module installer path (ProcessModuleInstall::unzipModule()
).
Language Support Upload Path (DoS via unbounded unzip)
Where: Setup → Languages → [Language] → (Core Translation Files | Site Translation Files)
These two file inputs are configured with unzip=1
and persist files under site/assets/files/<language_page_id>/
.
Core flow (per upload):
WireUpload::saveUploadZip()
- Creates temp extraction dir:
site/assets/files/<id>/.zip_tmp/
- Immediately calls
$files->unzip($zipFile, $tmpDir)
(no pre-validation)
- Creates temp extraction dir:
WireFileTools::unzip()
- Iterates
ZipArchive
entries and callsextractTo($tmpDir, $name)
for each - Only guard: reject names containing
'..'
- No limits on total uncompressed bytes, entry count, depth, or extraction time
- Iterates
- Post-extract pass (still in
saveUploadZip()
):- Walks extracted names and keeps/deletes based on allowed extensions (e.g.,
json,csv
) - Any deletions occur after extraction; temp dir is removed at the end
- Walks extracted names and keeps/deletes based on allowed extensions (e.g.,
Effect in this surface:
A small, highly-compressible .zip
(below typical upload caps, e.g., 40 MB) containing thousands of large *.json
entries expands to multi-GB in …/.zip_tmp/
during step (2), causing request-time CPU/disk spikes and observable slowdown across admin/site. Whether files are later rejected or deleted but the resource burn has already occurred.
Nested ZIP handling:
- When the outer archive contains multiple nested
*.zip
files, the cleanup phase removes only the first nested zip it processes; subsequent nested zips remain in the page’s files directory (e.g.,site/assets/files/<id>/nested_X.zip
). - This leads to residual artifacts under
site/assets/files/<id>/
(storage accumulation / arbitrary file planting), even though nested zips are not recursively extracted by core.
Example Zip Bomb
poc_bomb.zip
Impact
Even when limited to users who hold the lang-edit
permission, this flaw has high availability impact:
-
Request-time DoS (CPU + disk): A
.zip
well below common upload limits (e.g., ≤ 40 MB) can inflate to multi-GB during extraction in
site/assets/files/<language_id>/.zip_tmp/
, spiking CPU and I/O for the duration of the request. Other PHP requests (front-end and admin) slow down or time out while extraction runs. -
Disk exhaustion cascade: If the extraction grows to the partition’s free space, the site can hit ENOSPC:
- caching, session writes, image transformations, and log writes begin to fail → 500 errors and forced logouts
- background jobs that write under
site/assets/
also fail
This occurs before post-extraction cleanup is attempted.
-
Low-privilege, realistic actor:
lang-edit
is commonly granted to translators or content staff (sometimes external vendors). A compromised translator account or a malicious insider can trigger the DoS without admin privileges. -
Repeatable and parallelizable: Multiple uploads in parallel (or repeated uploads) produce additive pressure. Because extraction is unbounded and synchronous a small number of requests can saturate PHP-FPM workers and the disk.
-
Persistent storage abuse (nested ZIPs): In practice, the Language path’s cleanup removes only the first nested
*.zip
it encounters; additional nested zips remain insite/assets/files/<language_id>/
. Over time this enables unbounded storage growth and arbitrary file planting under a web-reachable path (even though core does not recurse into them).