DoS: WireFileTools::unzip() extracts archives before validation with no size/entry limits → lang-edit user can trigger GB-scale expansion
CVE-2025-60790 — ProcessWire 3.0.246 DoS via unlimited pre-validation ZIP extraction in Language Support
- Components: WireUpload::saveUploadZip(), WireFileTools::unzip()
- Who can exploit: User with
lang-edit - Impact: Resource-exhaustion (CPU/disk)
- Affected versions: 3.0.246 (earlier/later untested)
- Reporter: Md. Moniruzzaman Prodhan (NomanProdhan)
- CWE: CWE-409 (Improper Handling of Highly Compressed Data)
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
ZipArchiveentries 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
*.zipfiles, 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
.zipwell 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-editis 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
*.zipit 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).
@NomanProdhan Thanks for your issue report. This functionality is part of the admin, which is exclusively a trusted/administrative user environment. We trust the user enough not to upload a raunchy photos or viruses, or misuse the admin tools to cause a ruckus. If such a bad user were in the admin, there would be more effective ways for them to cause trouble than uploading an ugly zip file. But if improvements can be made to the existing zip functions I definitely welcome any suggestions or PRs for it, and it sounds like you've got some good ideas.
I don't see that ZipArchive has the ability to do what you are suggesting, though I could be wrong. But if you'd be interested in exploring it further, I'd be fine if there's a pre-validation that just accepts or rejects the zip as a whole, rather than extracting what it likes and skipping what it doesn't. We also have FileValidator modules, which are designed for this purpose (validating a file before accepting it as an upload), so maybe a FileValidatorZip module would be a potential way to implement. More details here: https://processwire.com/api/ref/file-validator-module/
@ryancramerdesign Thanks for the response. I’d like to clarify a few important points about impact and feasible fixes:
1. Not Admin-Only
This behavior is not restricted to full administrators.
The lang-edit permission is enough to trigger the vulnerable flow:
Setup → Languages → [Language] → Translation file upload- These fields are configured with
unzip=1and callWireUpload::saveUploadZip()directly.
In practice, lang-edit is often granted to translators, contractors, or other semi-trusted users.
A compromised translator account (weak password, phishing, etc.) can reliably cause site-wide DoS without requiring superuser privileges.
2. Denial of Service Vector
Because validation happens after extraction:
- A 20–40 MB crafted archive (below upload limit) can inflate to multi-GB in
site/assets/files/<id>/.zip_tmp/. - CPU, memory, and I/O spike during extraction → PHP-FPM workers stall → frontend/admin timeouts.
- If disk fills (
ENOSPC), sessions, logs, cache, and image writes fail → cascading 500 errors.
This is significantly more damaging than “uploading an ugly zip file.”
It’s a repeatable, parallelizable DoS vector in the core admin.
3. Persistent Storage Abuse
There’s a secondary issue with nested zips:
- Only the first nested
.zipis removed during cleanup. - Subsequent
.zipfiles remain in/site/assets/files/<id>/. - This leads to unbounded storage growth and arbitrary file planting in a web-accessible path.
So the issue is not only availability but also storage abuse + arbitrary file upload.
4. Feasible Fixes
While ZipArchive doesn’t provide built-in “safe unzip,” it does expose metadata we can use before extraction:
- Entry metadata via
statIndex()/statName():- Reject if total uncompressed size > configurable limit (e.g., 100 MB).
- Reject if entry count > N.
- Reject if path depth > safe threshold.
- Ensure all archive artifacts (including nested zips) are cleaned post-processing.
This would stop the DoS before it occurs, instead of attempting cleanup afterwards.
5. Proposal
As you suggested, this could be done via a FileValidatorZip module, but it must run before extractTo().
Otherwise the DoS occurs before validation.
A practical PR could:
- Add a
FileValidatorZipimplementingisValidFile()that:- Opens the archive, iterates entries with
statIndex(). - Sums
uncompressed_sizeand counts entries. - Rejects if thresholds are exceeded.
- Opens the archive, iterates entries with
- Wire it into
WireUpload::saveUploadZip()so validation runs before extraction. - Update cleanup logic to remove all nested zips, not just the first.
This keeps it modular (validator-style) but ensures extraction cannot be abused.
@NomanProdhan Some of your assumptions about PW's admin environment are not correct, and sounds like you might potentially be using it for users that fall outside its scope, or at least thinking of it as something different. Please see the following:
Any interest in developing a FileValidatorZIP module? Or collaborating?
Thanks for the clarification and background on how ProcessWire’s admin environment is intended to be used. I understand your point that the admin is designed for fully trusted users, and I acknowledge that this is the guiding philosophy behind PW’s architecture.
Just to be clear, my report was not about full administrators with superuser privileges. The issue is reachable with only the lang-edit permission, which in real deployments is often given to translators or other semi-trusted accounts. These users do not have the same level of trust or access as full admins, yet can still trigger the vulnerable ZIP upload path. From a security perspective, this broadens the attack surface beyond superusers.
I respect that your model assumes trusted admins, but technically this remains a weakness that could be mitigated with relatively little overhead by applying ZipArchive metadata pre-validation before extraction.
At the moment I’m focused on security research, so I won’t be able to dedicate time to building a full FileValidatorZip module, but I do think the pre-validation approach would be a strong enhancement if someone in the community (or later myself) is able to contribute a PR.
Thanks again for taking the time to review and respond.
Okay sounds good, I'll have a look at this soon to see about developing a FileValidatorZIP module. The benefit of a module in this case would be that if someone feels like they might benefit from it, it could be plugged-in to any existing installation, on just about any version of PW. So far there are only 2 FileValidator modules, and I've been looking for more opportunities to continue growing this module category.
Hey @ryancramerdesign!
Just wanted to point out that this is now a confirmed vulnerability, and roave/security-advisories (along with other similar projects) are flagging all ProcessWire versions up to 3.0.246 as vulnerable.
It would be great to have a solution to this, as these checks are causing a bit of headache for anyone relying on automated checks. Even if the system in question is not affected.
Thanks!
@teppokoivula @NomanProdhan Okay I have added a FileValidatorZip module to validate uploaded ZIP files. Also added several new options to the $files->unzip() method to make it more useful.
The module looks solid and it has eliminated the DoS issue.