usability.cat
Issue Wiki

Unrestricted File Upload

A mailbox that accepts packages of any size with no screening — that's file upload without restrictions. Time to add some rules.

What is this?

Imagine your mailbox has no size limit, no screening, and no return address verification. Anyone can drop off a package of any size containing anything — a letter, a bowling ball, or something dangerous. That is what an unrestricted file upload looks like. When your site accepts file uploads without checking file type, size, or contents, attackers can upload malicious scripts, executables, or absurdly large files that crash your server.

Why it matters

  • For your visitors: A malicious file uploaded to your server can be served to other visitors. An attacker uploads a disguised script file, your server serves it, and other visitors' browsers execute it. This can lead to session hijacking, malware distribution, or defacement of your site — all affecting your innocent visitors.
  • For your business: Unrestricted uploads can lead to remote code execution on your server (the attacker uploads a script and tricks your server into running it), storage abuse (someone fills your disk with 10GB files), and legal liability if your server ends up hosting malicious content. Cloud storage costs can spike dramatically from abuse.
  • The standard: Always validate file type (by content, not just extension), enforce maximum file sizes, store uploads outside the web root, generate new filenames (never use the original), and scan for malware when possible. Never trust the client-side validation alone — always validate server-side.
Validated file upload
// Server-side validation
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB

if (!ALLOWED_TYPES.includes(file.type)) {
  return { error: "Only JPEG, PNG, and WebP images allowed" };
}
if (file.size > MAX_SIZE) {
  return { error: "File must be under 5MB" };
}
Accept anything
// No validation at all — accepts any file of any size
async function handleUpload(file: File) {
  const formData = new FormData();
  formData.append("file", file);
  await fetch("/api/upload", { method: "POST", body: formData });
}

How to fix it

React / Next.js

Validate on both the client (for user experience) and server (for security).

// Client-side: immediate feedback
"use client";

const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
const MAX_SIZE_MB = 5;
const MAX_SIZE = MAX_SIZE_MB * 1024 * 1024;

function ImageUpload() {
  function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
    const file = e.target.files?.[0];
    if (!file) return;

    if (!ALLOWED_TYPES.includes(file.type)) {
      alert("Please upload a JPEG, PNG, or WebP image.");
      e.target.value = "";
      return;
    }

    if (file.size > MAX_SIZE) {
      alert(`File must be under ${MAX_SIZE_MB}MB.`);
      e.target.value = "";
      return;
    }

    uploadFile(file);
  }

  return <input type="file" accept=".jpg,.jpeg,.png,.webp" onChange={handleFileChange} />;
}

Server-side validation (the part that actually matters for security):

// src/app/api/upload/route.ts
import { randomUUID } from "crypto";
import path from "path";

const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
const MAX_SIZE = 5 * 1024 * 1024;

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file") as File | null;

  if (!file) {
    return Response.json({ error: "No file provided" }, { status: 400 });
  }

  // Validate MIME type
  if (!ALLOWED_TYPES.includes(file.type)) {
    return Response.json({ error: "Invalid file type" }, { status: 400 });
  }

  // Validate size
  if (file.size > MAX_SIZE) {
    return Response.json({ error: "File too large" }, { status: 400 });
  }

  // Generate a safe filename — NEVER use the original
  const ext = path.extname(file.name).toLowerCase();
  const safeFilename = `${randomUUID()}${ext}`;

  // Read and verify file contents (check magic bytes)
  const buffer = Buffer.from(await file.arrayBuffer());
  if (!isValidImage(buffer)) {
    return Response.json({ error: "File content invalid" }, { status: 400 });
  }

  // Store in a non-public location or cloud storage
  // await uploadToS3(safeFilename, buffer);

  return Response.json({ success: true, filename: safeFilename });
}

function isValidImage(buffer: Buffer): boolean {
  // Check magic bytes (file signatures)
  const jpeg = buffer[0] === 0xff && buffer[1] === 0xd8;
  const png = buffer[0] === 0x89 && buffer[1] === 0x50;
  const webp = buffer[8] === 0x57 && buffer[9] === 0x45;
  return jpeg || png || webp;
}

Plain HTML

Add client-side restrictions and always validate server-side.

<!-- Client-side: limit file types and provide guidance -->
<form action="/api/upload" method="POST" enctype="multipart/form-data">
  <label for="avatar">Upload avatar (JPEG or PNG, max 5MB):</label>
  <input type="file" id="avatar" name="avatar" accept=".jpg,.jpeg,.png,.webp" required />
  <button type="submit">Upload</button>
</form>
High impactsecurity~2 paws

An unrestricted file upload is an open invitation for attackers. The cat inspects every package before accepting it, and your server should too.

How the cat scores this

The scanner checks file upload inputs (<input type="file">) for the accept attribute, which indicates client-side file type filtering. It also looks at the associated form action endpoint for evidence of server-side validation. Upload forms without any accept attribute are flagged. The scanner notes whether maximum file sizes are communicated to the user. The severity is higher for forms that appear to handle sensitive uploads (profile pictures, documents) without restrictions.

Further reading

On this page