Skip to content

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

@NomanProdhan

Description

@NomanProdhan

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions