Skip to content

Replacing jQuery-File-Upload in PHP

blueimp/jQuery-File-Upload is one of the most widely deployed upload plugins ever written — and its repository is now archived. If it is handling uploads in your PHP application, here is what that actually means, how to check what you are exposed to, and what to move to.

What "archived" means here

The repository is read-only. No fixes, no security patches, no dependency updates — permanently. That is a different risk from "old but stable", because this is code that accepts files from the public internet, which is the single most attacked surface in a typical web application.

It is worth being accurate about the history rather than alarmist. The widely referenced CVE-2018-9206 was a remote-code-execution issue in the bundled PHP upload handler: the shipped example wrote uploads into a web-accessible directory and relied on a server configuration that could not be assumed, so an attacker could upload and then execute a script. It was fixed upstream in 2018. The reason it still matters is the fork problem — the handler was copied into thousands of projects and tutorials, and those copies never got the fix.

Audit what you actually have, in five minutes

Before planning any migration, establish which version and which handler are in your tree:

# which version is deployed?
grep -r "@version" --include="jquery.fileupload*.js" . | head

# is the PHP handler present, and is it the vulnerable shape?
find . -name "UploadHandler.php" -o -name "index.php" -path "*server*"

# the critical question: can uploaded files be executed?
#   - is upload_dir inside the web root?
#   - does the directory allow PHP execution?
grep -n "upload_dir\|upload_url" server/php/UploadHandler.php 2>/dev/null | head

Three findings decide your urgency:

If the first applies, the fastest mitigation is not a migration: move the directory outside the document root, or block execution there, today. The security checklist has the exact Apache, Nginx and IIS rules.

The options

Keep the UI, replace the server handler

The smallest change: keep the jQuery plugin in the browser and write your own endpoint. The plugin posts ordinary multipart requests, so a handler you control is 40 lines:

<?php
declare(strict_types=1);
header('Content-Type: application/json');

const DIR   = '/var/www/uploads';                 // outside the web root
const MAX   = 20 * 1024 * 1024;
const TYPES = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'application/pdf' => 'pdf'];

$out = ['files' => []];
foreach ($_FILES['files']['name'] ?? [] as $i => $name) {
    $err = $_FILES['files']['error'][$i];
    $tmp = $_FILES['files']['tmp_name'][$i];

    if ($err !== UPLOAD_ERR_OK || !is_uploaded_file($tmp) || $_FILES['files']['size'][$i] > MAX) {
        $out['files'][] = ['name' => $name, 'error' => 'Rejected'];
        continue;
    }
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($tmp);
    if (!isset(TYPES[$mime])) {
        $out['files'][] = ['name' => $name, 'error' => 'Type not permitted'];
        continue;
    }
    $stored = bin2hex(random_bytes(16)) . '.' . TYPES[$mime];
    move_uploaded_file($tmp, DIR . '/' . $stored);
    $out['files'][] = ['name' => $name, 'size' => $_FILES['files']['size'][$i]];
}
echo json_encode($out);

This removes the unmaintained server code — the part with the history — while leaving your front end untouched. It does not remove the unmaintained JavaScript, so treat it as a stepping stone.

Replace the front end too

If you are touching it anyway, the jQuery dependency is usually the other thing worth dropping:

The full comparison, with licences and release dates checked, is in PHP upload libraries compared.

Migrating without breaking existing files

Two details catch people during the swap:

The response shape. jQuery-File-Upload's UI expects {"files":[{"name":…,"size":…,"url":…}]}. If you keep the front end, your new endpoint must return that shape, or the UI reports success as failure.

Existing stored files. The old handler named files after what the user uploaded, so your database may hold user-supplied names as paths. Do not carry that forward: generate stored names from now on, keep the original as display data only, and treat historic paths as untrusted when reading them back.

// reading a legacy record safely
$stored = basename($row['filename']);              // strip any path
$path   = realpath(DIR . '/' . $stored);
if ($path === false || strpos($path, realpath(DIR)) !== 0) {
    throw new RuntimeException('Refusing to read outside the upload directory.');
}

If you would rather not own the upload path at all

PHP File Uploader replaces both halves with one component: it renders the browser UI and handles the PHP side, so there is no bundled handler to audit and no server contract to implement. It is commercial software, which is the trade against the MIT options above — but for a team migrating away from unmaintained upload code specifically because they do not want to maintain upload code, that is often the point.

<?php
require_once "phpuploader/include_phpuploader.php";

$uploader = new PhpUploader();
$uploader->MultipleFilesUpload = true;
$uploader->MaxSizeKB = 102400;
$uploader->AllowedFileExtensions = "jpg,png,pdf";
$uploader->Render();
?>

The demos run the real thing, including drag and drop and large files. Whatever you choose, the server-side rules are the same — and they are what the original CVE was about.