Unzip with PHP on Shared Hosting: A Safer Way to Extract ZIP Files

Illustrated infographic summarizing: Unzip with PHP on Shared Hosting: A Safer Way to Extract ZIP Files

By Greg Nowak. Updated 7 September 2026.

Uploading one ZIP archive can save time when a website release contains thousands of small files. If your shared hosting has no SSH access and its file manager cannot extract archives, PHP can handle the job.

The extraction itself is only one step. For a business website, the useful outcome is a complete release, a checked customer journey and a clear way back if something fails. Here is how to organise that work without overwriting the live site halfway through an update.

Choose the simplest available route

Check the hosting panel before adding a script. Its extraction feature may be sufficient, provided you can choose a separate destination. For an agency handover, record who uploads the package, who activates it and who checks the result.

Hosting situation Practical approach Before proceeding
File manager supports ZIP extraction Use the hosting panel Select a fresh release directory
No SSH or panel extraction; PHP ZIP is available Use a temporary PHP helper Protect access and staging files
PHP ZIP is missing Ask the host to enable it or extract the package Confirm support for the site's PHP runtime
Large archives or repeated timeouts Ask for a host-managed or command-line operation Check disk quota and execution limits
Choose the extraction method around the hosting account's actual capabilities.

Check PHP, space and access first

With shell access, check ZIP support using:

php -m | grep -i zip

The command-line runtime may differ from the website's PHP configuration. For a browser helper, check the hosting panel or temporarily run var_dump(class_exists('ZipArchive')); behind access protection, then delete it. The PHP installation documentation explains the extension requirements; shared-hosting changes usually belong with the host.

Use a supported PHP branch with current patches. On 7 September 2026, PHP 8.2–8.5 remain supported; 8.2 and 8.3 receive security fixes only. PHP 8.2 support ends on 31 December 2026, according to the official support schedule.

Allow space for the ZIP, expanded release and previous release, plus working headroom. Check the account's file-count quota too. A small compressed file can contain many entries.

Before uploading the helper, protect its URL with hosting-panel authentication over HTTPS or an appropriate IP restriction. An obscure filename is insufficient. Keep the archive and staging directory outside the public document root where possible; otherwise block web access to both. Filesystem permissions alone do not stop the web server serving files it can read.

A bounded ZipArchive helper for trusted releases

This example is for a team-produced, reviewed archive on Linux shared hosting. Place it in the protected maintenance workspace described above. It uses fixed paths, rejects questionable entry names and requires a new destination. Adjust the example limits to the expected package.

<?php
$archive = __DIR__ . '/site-release.zip';
$target = __DIR__ . '/release-2026-09-07';
$maxEntries = 2000;
$maxBytes = 200 * 1024 * 1024; // 200 MiB, declared size

if (!class_exists('ZipArchive')) {
    exit('ZIP support is unavailable.');
}
if (!is_file($archive)) {
    exit('Archive not found.');
}
if (file_exists($target) || is_link($target)) {
    exit('Use a new release directory.');
}

$zip = new ZipArchive();
$result = $zip->open(
    $archive,
    ZipArchive::RDONLY | ZipArchive::CHECKCONS
);
if ($result !== true) {
    exit('Could not open archive. Error code: ' . $result);
}

$oldUmask = umask(0077);
try {
    if ($zip->numFiles > $maxEntries) {
        throw new RuntimeException('Too many archive entries.');
    }
    $total = 0;
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $stat = $zip->statIndex($i);
        if ($stat === false) {
            throw new RuntimeException('Cannot inspect entry.');
        }
        $path = $stat['name'];
        if ($path === '' || $path[0] === '/'
            || str_contains($path, '\\')
            || str_contains($path, ':')
            || preg_match('/[\x00-\x1F\x7F]/', $path)
            || preg_match('#(^|/)\.{1,2}(/|$)#', $path)) {
            throw new RuntimeException('Unsupported archive path.');
        }
        $size = $stat['size'];
        if (!is_int($size) || $size < 0
            || $size > $maxBytes - $total) {
            throw new RuntimeException('Declared size exceeds limit.');
        }
        $total += $size;
    }
    if (!mkdir($target, 0700)) {
        throw new RuntimeException('Cannot create release directory.');
    }
    if (!$zip->extractTo($target)) {
        throw new RuntimeException('Extraction failed; inspect partial release.');
    }
    echo 'Extracted. Validate before activation.';
} catch (RuntimeException $e) {
    http_response_code(500);
    echo htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
} finally {
    umask($oldUmask);
    $zip->close();
}

The strict !== true check matters because ZipArchive::open() can return an integer error code. Consistency checking does not establish that the contents are trustworthy.

The size check is preflight validation, not a hard extraction quota. statIndex() supplies archive metadata; this helper does not count bytes as they are written. It is unsuitable for processing arbitrary customer uploads or defending against hostile archives.

The restrictive umask keeps staging permissions narrow. PHP documents that extractTo() does not restore original permissions. Before activation, apply the host's required file and directory permissions, including any executable scripts. Avoid blanket chmod 777 fixes.

Validate the release before switching traffic

Check the directory layout: an extra parent folder can put index.php one level below where the site expects it. Confirm dependencies, environment configuration and writable directories. Keep uploads, secrets and generated content outside the replaceable package wherever practical.

Agree the activation method before starting. Some hosts allow a document-root change; others require a maintenance window and directory changes. Do not assume switching will be instantaneous. Keep the previous release and test the homepage, a key enquiry or purchase journey, and the error log after activation.

If the release changes the database, restoring old files may not restore the application. Plan compatible migrations or a tested database recovery procedure, accounting for orders and submissions received since the backup.

Close the maintenance job properly

A timeout or failed extraction can leave a partial directory. Inspect it, remove the failed release and retry into a fresh destination. Delete the temporary helper and uploaded ZIP after the job; record the release, checks and rollback location.

If these deployments depend on one person's memory, Greg can help document and coordinate a repeatable release workflow that fits your hosting, team and budget.

Related on GrN.dk

Need help with this kind of work?

Discuss your website deployment workflow Get in touch with Greg.

Sources

Latest articles

NGINX 1.31.5 can route on JSON body values. Here’s how to weigh the performance, security, and operational trade-offs before using it.

OpenAI can keep agent sessions running, but reliable workflows still depend on clear failure states, safe retries, validation, limits and human fallback.

AI can identify termination deadlines and price adjustments in supplier contracts, route uncertain findings for approval and create the right reminders.

Why a DNS record can exist in a dashboard yet fail publicly—and how to trace zone cuts, verify glue, and fix the right side of a live delegation.

An Apache version below 2.4.68 may still be patched. Package provenance, vendor advisories, module checks and runtime evidence reveal the real position.

PHP 8.2 security support ends on December 31, 2026. Here is how to audit, test, and migrate a mixed CMS estate without rushing production changes.

How Danish businesses can automate Gmail and Microsoft 365 with rapid sorting, limited permissions and human approval.

When WordPress jobs run late, check WP-Cron and queue capacity first. Diagnose triggers, handlers, and Action Scheduler without guesswork.

WordPress 7.1 makes speculative loading configurable. Here’s how to spot overlapping rules and test speed gains without adding hidden costs.

Multiple records for the same customer in HubSpot? Learn how CVR number matching, AI suggestions and human approval can help you clean up duplicates while keeping track of fields, associations and customer history.