Skip to main content
Blog

Web

File upload vulnerabilities and how to defend against them

An avatar or document upload form can open the door to code execution on the server. Practical rules for type checks, storage location and file naming.

· 8 min read

Photo: unspecified, CC0 · Wikimedia Commons

Almost every web application accepts file uploads: a profile photo, a scanned contract, an Excel report, an attachment to an application. It looks like a simple feature, but built incorrectly it becomes one of the most dangerous vulnerabilities: a user places their own file on the server, and the server stores it and sometimes even runs it.

OWASP describes this risk separately as "Unrestricted File Upload", and CWE lists it as CWE-434 (Unrestricted Upload of File with Dangerous Type). During an expertise, upload forms usually get particular attention.

What can go wrong

1. Code execution on the server

The worst case. A user uploads a file named photo.php or photo.jpg.php, it lands in the web server's public folder, the user then opens https://example.uz/uploads/photo.php, and the PHP code runs on the server. This can give an attacker full control of the server.

2. Stored XSS

SVG and HTML files can contain JavaScript. If such a file is opened in the browser from the site's own domain, the script runs as that site, with the user's session.

3. Path traversal

If the file name is something like ../../config/app.php and the server inserts it directly into a path, the file can be written somewhere unintended or overwrite an existing file.

4. Denial of service

Very large files, too many files, or archives such as a "zip bomb" fill up disk and memory and slow the server down.

5. Access to other people's files

If uploaded personal documents are stored in a public folder with predictable names (passport_15.pdf), other users can open them. This is really a form of IDOR.

The core rule: an uploaded file is untrusted data. Never execute it, never trust its name, and never store it in a public folder under the name the user gave it.

Layers of defence

An allowlist of permitted types

A list of forbidden extensions ("no .php") does not work: there is always .phtml, .php5, .phar and other variants. Instead, allow only the types you need and check the file's actual content (its MIME type), not just its extension.

In Laravel:

$request->validate([
    'avatar'   => ['required', 'file', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
    'contract' => ['required', 'file', 'mimetypes:application/pdf', 'max:10240'],
]);

The mimes rule determines the MIME type from the file's content. max is in kilobytes. For images, the image and dimensions rules are also useful.

The server chooses the file name

Do not use the user-supplied name for storage. Generate a random name and keep the original name (if needed) only in the database, for display:

$path = $request->file('contract')->store('contracts', 'private');
// result: contracts/7Hq2...xZ.pdf

Document::create([
    'user_id'       => $request->user()->id,
    'path'          => $path,
    'original_name' => $request->file('contract')->getClientOriginalName(),
]);

store() assigns a random name and an extension matching the content, which also closes path traversal.

Store outside the public folder

Personal documents should not live in public/. In Laravel, use a disk such as storage/app/private and serve the file through a controller that checks ownership:

public function download(Document $document)
{
    $this->authorize('view', $document);

    return Storage::disk('private')->download(
        $document->path,
        $document->original_name
    );
}

There is then no direct link to the file, and every download goes through authorization.

Forbid script execution in the upload folder

If images must live in a public folder (avatars, for example), the web server must not execute any scripts there. For nginx:

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

This is the second line of defence: even if validation lets a file through for some reason, it will not run as code.

Be careful with SVG and HTML

If you do not need SVG, do not add it to the allowlist. When serving user files, set safe headers:

add_header X-Content-Type-Options "nosniff" always;

For downloadable documents, Content-Disposition: attachment tells the browser not to open the file as a page. Where possible, serve user files from a separate domain.

Re-process images

Re-encoding an uploaded image (for example, resizing and saving it again) strips hidden extra data and metadata (EXIF, including GPS coordinates).

Size and count limits

Besides the max rule in the application, set limits at the web server and PHP level:

client_max_body_size 10m;
upload_max_filesize = 10M
post_max_size = 12M

Add rate limiting to upload routes.

Checklist

  • Are only the needed file types allowed, and are they checked by content?
  • Is the file name generated on the server side?
  • Are personal documents stored outside the public folder?
  • Is ownership checked on download?
  • If a PHP file in the upload folder is requested, is it executed or rejected?
  • Can SVG and HTML be uploaded, and how are they served?
  • Is there a size limit at the application, PHP and web server level?

You can safely test the last items yourself: try uploading a harmless test.php (for example, one that only prints some text) and watch what happens, on your own test environment only.

Summary

Upload security does not rest on a single rule but on several layers: validation, naming, storage location, web server configuration and authorization. If one layer lets something through, the next one stops it. In our process, the Claude AI agent quickly reviews every upload point, the validation rules and the server configuration, and the expert tests them in practice and applies the fixes.

If you want to check the security of the upload forms in your project, request a security scan. We will review them and help close the gaps before the official expertise.