processwire-issues icon indicating copy to clipboard operation
processwire-issues copied to clipboard

DoS: WireFileTools::unzip() extracts archives before validation with no size/entry limits → lang-edit user can trigger GB-scale expansion

Open NomanProdhan opened this issue 4 months ago • 8 comments

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):

  1. WireUpload::saveUploadZip()
    • Creates temp extraction dir: site/assets/files/<id>/.zip_tmp/
    • Immediately calls $files->unzip($zipFile, $tmpDir) (no pre-validation)
  2. WireFileTools::unzip()
    • Iterates ZipArchive entries and calls extractTo($tmpDir, $name) for each
    • Only guard: reject names containing '..'
    • No limits on total uncompressed bytes, entry count, depth, or extraction time
  3. 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

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 in site/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 avatar Aug 20 '25 06:08 NomanProdhan

@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 avatar Aug 29 '25 16:08 ryancramerdesign

@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=1 and call WireUpload::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 .zip is removed during cleanup.
  • Subsequent .zip files 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:

  1. Add a FileValidatorZip implementing isValidFile() that:
    • Opens the archive, iterates entries with statIndex().
    • Sums uncompressed_size and counts entries.
    • Rejects if thresholds are exceeded.
  2. Wire it into WireUpload::saveUploadZip() so validation runs before extraction.
  3. 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 avatar Aug 29 '25 16:08 NomanProdhan

@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?

ryancramerdesign avatar Aug 29 '25 18:08 ryancramerdesign

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.

NomanProdhan avatar Aug 29 '25 18:08 NomanProdhan

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.

ryancramerdesign avatar Aug 29 '25 20:08 ryancramerdesign

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 avatar Oct 28 '25 06:10 teppokoivula

@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.

ryancramerdesign avatar Nov 07 '25 18:11 ryancramerdesign

The module looks solid and it has eliminated the DoS issue.

NomanProdhan avatar Nov 08 '25 16:11 NomanProdhan