Current situation: PHP2Shell. Two critical PHP vulnerabilities (CVSS 9.2) allow malicious code without login. Patches since 21 October 2026.What to do now →

Implementing file uploads in PHP safely: PHP upload security

Upload forms are a frequent route for webshells. How to check file types properly, store files safely and stop the server from executing uploaded scripts.

Last updated 6 min readGuides

In short: For PHP upload security, it is not a single check that decides but the combination: check the file type on the server, rename files, store them outside the web root and allow no script execution in the upload folder. An unchecked upload form is one of the classic routes by which attackers bring a backdoor onto a web server.

Why upload forms are a preferred entry point

Every form that accepts files writes foreign data onto your server. These include application forms, customer portals, image galleries, support areas and many CMS and shop extensions. If an attacker manages to place a file there that the web server then executes as PHP, they have a permanent backdoor.

For management this means: an innocent-looking upload for résumés can, in the worst case, lead to the takeover of the entire web server. In addition, upload folders often hold personal data that is publicly retrievable if configured wrongly.

For admins and developers this means: application code and server configuration must fit together. A weakness at one level is often caught by the other when both are implemented cleanly. How to find files that have already been planted is described in the guide Detecting and removing webshells.

The basic rules for PHP upload security

The following rules correspond to the recommendations of the OWASP Cheat Sheets and have proved their worth in operation:

  • Allow-list instead of block-list: Accept only explicitly permitted file types, for example PDF, JPG and PNG. Block-lists of individual extensions can be bypassed via variants and double extensions.
  • Check the content, not just the extension: Determine the actual type on the server with finfo. The MIME type in $_FILES is a statement by the sender and is not trustworthy.
  • New file name: Never use the original name as the file name. Generate a random name and append the extension from the allow-list.
  • Storage outside the web root: Store files so that they are not directly reachable by URL. Delivery goes through a script with a permission check.
  • Limit the size: In the application, in PHP and in the web server.
  • No execution: If uploads must lie in the web root after all, switch off script execution in that directory.
  • Check only on the server: JavaScript checks in the browser improve usability but do not protect.

A secure example with finfo and a random name

The following example shows the core of secure handling. It is deliberately brief and must be embedded in your application, that is, with login, permission check, CSRF protection and logging.

<?php
declare(strict_types=1);

const MAX_BYTES  = 5 * 1024 * 1024; // 5 MB
const ERLAUBT    = [
    'application/pdf' => 'pdf',
    'image/jpeg'      => 'jpg',
    'image/png'       => 'png',
];
const ZIELORDNER = '/var/www/daten/uploads'; // outside the web root

$datei = $_FILES['dokument'] ?? null;

if (!is_array($datei) || $datei['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit('Upload fehlgeschlagen.');
}

if ($datei['size'] > MAX_BYTES || !is_uploaded_file($datei['tmp_name'])) {
    http_response_code(400);
    exit('Datei ungültig.');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$typ   = $finfo->file($datei['tmp_name']);

if (!isset(ERLAUBT[$typ])) {
    http_response_code(415);
    exit('Dateityp nicht erlaubt.');
}

$name = bin2hex(random_bytes(16)) . '.' . ERLAUBT[$typ];
$ziel = ZIELORDNER . '/' . $name;

if (!move_uploaded_file($datei['tmp_name'], $ziel)) {
    http_response_code(500);
    exit('Speichern fehlgeschlagen.');
}

chmod($ziel, 0640);
// Store the original name only as text in the database
// and escape it with htmlspecialchars() on every output.

With images, one further step is worthwhile: re-encode the image with GD or Imagick and discard the metadata in the process. This does not replace the other checks but lowers the residual risk from embedded foreign data.

For documents from unknown sources, such as applications or complaint photos, a virus scan before passing them on also makes sense. It protects the web server less than the employees who later open the files on their workstations. Also log every upload with time, account, file size and detected type, so that unusual patterns can be traced later.

With CMS and shops, the built-in media libraries usually bring their own checks. Riskier are form extensions, import functions and self-programmed add-on pages that store files bypassing the library. These are exactly the places you should look at first in a review.

Tip

Deliver files from the protected folder with Content-Disposition: attachment and X-Content-Type-Options: nosniff. The browser then does not display problematic content directly but offers it as a download.

Switching off PHP execution in the upload folder

Even if a file with wrong content slips through, the web server must not execute it as a script. This second layer of protection belongs on every server that stores uploads in the web root.

For Nginx with PHP-FPM:

location ^~ /uploads/ {
    location ~* \.(php|phtml|phar)$ {
        deny all;
    }
}

For Apache, ideally directly in the VirtualHost rather than in a .htaccess, so that the rule cannot be overridden by the application:

<Directory "/var/www/html/uploads">
    AllowOverride None
    <FilesMatch "\.(php|phtml|phar)$">
        Require all denied
    </FilesMatch>
</Directory>

If PHP runs as an Apache module (mod_php), you can additionally set php_admin_flag engine off in the same block. After the change, use a harmless test file that only outputs a text to check that the server answers a request for it with 403.

Uploads are closely tied to the PHP settings post_max_size and upload_max_filesize. In the wake of PHP2Shell, php.net recommends as an interim measure setting post_max_size to 8M if no uploads are needed, or to 32M with uploads, and keeping upload_max_filesize below it. This is not complete protection and does not replace the update to a fixed PHP version.

; php.ini or FPM pool (php_admin_value), then restart all workers
file_uploads        = On
post_max_size       = 32M
upload_max_filesize = 20M
max_file_uploads    = 5

If a website needs no uploads at all, file_uploads = Off is the cleanest solution. Which settings make sense until the patch is under Immediate measures without a patch. Suitable permissions for upload directories are described in File permissions on the web server.

Typical mistakes in upload functions

The same patterns keep turning up when reviewing existing applications. Check your forms specifically for these points:

  • The extension is checked only partially, so file names with several extensions slip through.
  • The original name from the form ends up in the storage path. That opens the way to path manipulation and overwriting of existing files.
  • Uploads lie publicly with predictable names. With applications or contracts this is also a data protection problem.
  • The upload folder is writable for the web server and executable at the same time.
  • Old upload extensions or form plugins are not updated although the vendor has released security updates.
  • Errors are not logged. Rejected uploads, however, are an early signal of targeted attempts.

How does PHP SOS help?

  • We check your upload functions in the code and in the server configuration and name concrete weaknesses with priorities.
  • We set the execution block in the upload folder, suitable permissions and size limits as part of server hardening.
  • If there is a suspicion, we search upload directories for planted scripts and clean them up through our malware removal.
  • On request, we have the implementation independently cross-checked by a penetration test.

Frequently asked questions

Is it enough to check the file extension on upload?

No. The extension and the MIME type reported by the browser come from the sender and can be chosen freely. Check the content on the server, assign a new file name and block script execution in the upload directory.

Where should uploaded files be stored?

Ideally outside the public web directory. Delivery is then handled by a script that first checks whether the user may see the file. That way files can be neither requested directly nor executed.

Are SVG files harmless as images?

Not automatically. SVG is XML and can contain active content. Allow SVG only if you sanitise the files or deliver them exclusively as a download with a suitable Content-Disposition header.

Does upload size have anything to do with PHP2Shell?

Yes, indirectly. As an interim measure, php.net recommends lower values for post_max_size and upload_max_filesize. This reduces the risk but does not replace the update to a fixed PHP version and the restart of all workers.

Sources

Immediate contact

Every hour counts. Write to us now.

Briefly describe your system, PHP version and observation. We will get back to you, clarify the situation and work remotely until the incident is closed and documented.

Immediate help