Insecure File Uploads in AI-Generated Code: No Validation, No Limits - VibeDoctor 
← All Articles 🔒 Security Vulnerabilities High

Insecure File Uploads in AI-Generated Code: No Validation, No Limits

AI tools generate file upload endpoints without type validation or size limits. Learn how attackers exploit this and how to fix it.

SEC-011

Quick Answer

AI-generated file upload endpoints routinely accept any file type at any size without validation. This lets attackers upload executable scripts disguised as images, exhaust server disk space, and cause denial-of-service conditions. A secure upload handler must validate MIME type from the file's magic bytes (not the filename), enforce a maximum file size, and store uploads outside the web root.

Why File Uploads Are One of the Riskiest Features to Vibe-Code

File uploads are one of the top three most dangerous features in web applications, alongside authentication and database queries. Yet they're also one of the features developers most frequently build with AI tools - "add a profile picture upload" or "let users upload documents" are common prompts. The AI generates working code fast, and it works perfectly for legitimate users. For an attacker, an unvalidated upload endpoint is an open door.

OWASP's security misconfiguration category covers unrestricted file uploads and includes it as a critical-severity pattern. According to Veracode's 2023 State of Software Security report, file handling vulnerabilities appear in 16% of all web applications - and the rate is growing as more apps add upload functionality through AI-generated code.

Apiiro's 2023 AI code security study found that features with complex security requirements - file handling, authentication, and payment integration - showed the highest rates of vulnerabilities in AI-generated code, with upload-related security gaps appearing 3x more often than in human-written code for equivalent features. The reason: secure file handling requires multiple coordinated checks, and AI tools reliably implement the functional part while omitting the security checks.

What AI-Generated Upload Code Looks Like

Here's the typical output from Bolt or Cursor when asked to "add file upload to my Next.js app":

// ❌ BAD - No file type validation, no size limit
// Next.js App Router: app/api/upload/route.ts
import { writeFile } from 'fs/promises';
import path from 'path';

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

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

  // No MIME type check - accepts .php, .exe, .sh, anything
  // No file size limit - attacker can upload multi-GB files
  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  // Stored in public directory - directly accessible via URL!
  const filepath = path.join(process.cwd(), 'public/uploads', file.name);
  await writeFile(filepath, buffer);

  // Returns the public URL - attacker can execute uploaded scripts
  return Response.json({ url: `/uploads/${file.name}` });
}

// ❌ BAD - Express Multer without validation
import multer from 'multer';

const upload = multer({ dest: 'uploads/' }); // No limits, no filter

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ filename: req.file.filename });
});

This code has four distinct vulnerabilities: no file type validation (an attacker uploads a PHP web shell), no size limit (disk exhaustion attack), storage in the public directory (the uploaded file is directly accessible via URL), and the original filename is used (path traversal via filenames like ../../server.js).

The Attack Scenarios

Attack How it works Missing control
Web shell upload Upload shell.php, execute via URL to run server commands File type validation
MIME type spoofing Rename malware.exe to image.jpg, bypass extension checks Magic byte validation
Disk exhaustion (DoS) Upload hundreds of 1 GB files until disk is full File size limit
Path traversal Upload with filename ../../config.js to overwrite server files Filename sanitization
Stored XSS via SVG Upload SVG with embedded <script> tag, serve to other users SVG sanitization or blocking
Server-side execution Upload script to web root, access via URL to execute Storage outside web root

How to Build a Secure Upload Handler

A secure file upload implementation has five layers: size limits enforced before reading the file, MIME type validation from magic bytes, filename sanitization, storage outside the web root (or in object storage), and - for images specifically - re-encoding through a processing library to strip embedded payloads.

// ✅ GOOD - Secure file upload in Next.js App Router
import { writeFile } from 'fs/promises';
import path from 'path';
import { randomUUID } from 'crypto';

const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);

// Magic bytes for type validation (can't be faked like filename/Content-Type)
const MAGIC_BYTES: Record = {
  'image/jpeg': [0xFF, 0xD8, 0xFF],
  'image/png':  [0x89, 0x50, 0x4E, 0x47],
  'image/webp': [0x52, 0x49, 0x46, 0x46],
};

function detectMimeFromBytes(buffer: Buffer): string | null {
  for (const [mime, magic] of Object.entries(MAGIC_BYTES)) {
    if (magic.every((byte, i) => buffer[i] === byte)) return mime;
  }
  return null;
}

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

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

  // 1. Enforce size limit before reading full content
  if (file.size > MAX_FILE_SIZE) {
    return Response.json(
      { error: 'File too large. Maximum 5 MB.' },
      { status: 413 }
    );
  }

  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  // 2. Validate MIME type from magic bytes (not filename or Content-Type)
  const detectedType = detectMimeFromBytes(buffer);
  if (!detectedType || !ALLOWED_TYPES.has(detectedType)) {
    return Response.json(
      { error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' },
      { status: 415 }
    );
  }

  // 3. Generate a safe, random filename - never use the original
  const ext = detectedType.split('/')[1];
  const safeFilename = `${randomUUID()}.${ext}`;

  // 4. Store outside public directory (or use Vercel Blob / S3)
  const uploadDir = path.join(process.cwd(), 'private/uploads');
  await writeFile(path.join(uploadDir, safeFilename), buffer);

  // Return a URL that goes through your own serving endpoint (access-controlled)
  return Response.json({ fileId: safeFilename });
}

For production apps on Vercel, use Vercel Blob (@vercel/blob) or AWS S3 instead of writing to the local filesystem. Object storage handles scaling, CDN delivery, and access control - and it removes the possibility of path traversal to local server files entirely.

Configuring Multer Securely for Express

// ✅ GOOD - Multer with size limits and file filter
import multer from 'multer';
import { randomUUID } from 'crypto';
import path from 'path';

const upload = multer({
  storage: multer.diskStorage({
    destination: '/var/private/uploads', // Outside web root
    filename: (req, file, cb) => {
      // Never use file.originalname - generate a safe name
      const ext = path.extname(file.originalname).toLowerCase();
      cb(null, `${randomUUID()}${ext}`);
    },
  }),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5 MB max
    files: 1,                  // Only one file per request
  },
  fileFilter: (req, file, cb) => {
    const allowedMimes = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowedMimes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  },
});

// Note: Multer's fileFilter checks the Content-Type header, not magic bytes.
// For defense in depth, also validate magic bytes after upload.

One important caveat: Multer's fileFilter checks the Content-Type header sent by the client - which an attacker can set to anything. For true type validation, read the first few bytes of the saved file and verify against the magic byte signatures after upload.

How to Find Insecure Upload Endpoints in Your Codebase

Search your project for formData.get, multer(, upload.single, writeFile, and createWriteStream. For every file-writing operation, verify that: (1) a size check occurs before or during reading, (2) the file type is validated, (3) the filename is regenerated (not taken from the upload), and (4) the storage location is outside the web root.

Tools like VibeDoctor (vibedoctor.io) automatically scan your codebase for insecure file upload patterns and flag specific file paths and line numbers. Free to sign up.

Also review your Vercel or server configuration. If you have an uploads folder inside your public directory, any file stored there is directly accessible via URL - which is the right setup for intentionally public assets, but a critical vulnerability for user-uploaded content that may contain malicious payloads.

FAQ

Why can't I just check the file extension?

File extensions are user-controlled metadata. An attacker can rename webshell.php to photo.jpg and upload it. The server stores it with the .jpg extension, but if it's served as PHP or accessed directly, the content executes. Magic byte validation reads the actual file content - the first 4-12 bytes that identify the file format - which cannot be faked without corrupting the file.

What's the recommended file size limit for profile images?

5 MB is a reasonable limit for unprocessed images from phone cameras. If you're processing images with Sharp or similar (recommended), reduce the input limit to 10 MB to allow raw photos, then output optimized WebP at much smaller sizes. Always set both a per-file limit and a per-request limit when accepting multiple files.

Should I use Vercel Blob or AWS S3 for uploads?

Both are good choices. Vercel Blob is the simplest option for apps deployed on Vercel - zero config, CDN-backed, and integrates with Next.js naturally. AWS S3 gives more control, fine-grained IAM permissions, lifecycle policies, and is cheaper at scale. Avoid writing to the local filesystem in serverless environments - file writes don't persist across function invocations on Vercel or similar platforms.

Are SVG files safe to allow for upload?

SVG files are XML and can contain embedded JavaScript via <script> tags. If you allow SVG uploads and serve them with a Content-Type: image/svg+xml header, the browser executes the embedded scripts - stored XSS. Either block SVG entirely or sanitize uploaded SVGs with a library like DOMPurify (server-side) before storage and serving.

What about PDF uploads - are those safe?

PDFs can contain embedded JavaScript, links to external resources, and malicious payloads that exploit PDF reader vulnerabilities. For user-uploaded PDFs, validate the magic bytes (%PDF-), enforce strict size limits, scan with ClamAV if possible, and never auto-execute or render them inline. Store them in private object storage and serve via signed URLs with content-disposition: attachment headers.

Scan your codebase for this issue - free

VibeDoctor checks for SEC-011 and 128 other issues across 15 diagnostic areas.

SCAN MY APP →
← Back to all articles View all 129+ checks →