Malicious File Upload
File upload functionality can be a powerful attack vector when improperly secured. Attackers can upload malicious files to achieve remote code execution, deface websites, store illegal content, or use your server as a malware distribution point. In April 2025, a critical SAP NetWeaver vulnerability allowed unauthenticated attackers to upload files and run arbitrary commands.
What is File Upload Vulnerability?
File upload vulnerabilities occur when applications accept file uploads without properly validating the file type, content, and destination. This allows attackers to:
- Upload web shells for remote code execution
- Upload HTML/SVG files containing XSS payloads
- Overwrite critical system files
- Upload files that exploit image processing libraries
- Consume server resources with oversized files
Common Attack Techniques
Web Shell Upload
<?php
// Simple PHP web shell - shell.php
if(isset($_GET['cmd'])) {
echo '<pre>' . shell_exec($_GET['cmd']) . '</pre>';
}
?>
// Attacker uploads this as 'profile.php'
// Then visits: /uploads/profile.php?cmd=whoami
// Gets full command execution on serverExtension Bypass Techniques
# If server only blocks .php extension:
shell.php.jpg # Double extension
shell.php%00.jpg # Null byte injection (older systems)
shell.pHp # Case variation
shell.php5 # Alternative PHP extensions
shell.phtml # PHP in HTML
shell.php. # Trailing dot (Windows)
shell.php::$DATA # NTFS alternate data stream
# Content-Type manipulation
# Set Content-Type: image/jpeg but upload PHP codeMagic Bytes Bypass
// File starts with GIF magic bytes but contains PHP
GIF89a
<?php system($_GET['cmd']); ?>
// Server checks file signature, sees 'GIF89a'
// Allows upload thinking it's an image
// But file is actually executable PHPSVG XSS Attack
<!-- malicious.svg - executes JavaScript when viewed -->
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg"
onload="alert(document.cookie)">
<rect width="100" height="100"/>
</svg>
<!-- If served from same origin, steals cookies -->Vulnerable Code Examples
No Validation
// VULNERABLE: No validation at all
app.post('/upload', upload.single('file'), (req, res) => {
// File saved directly to public uploads folder
// with original filename!
const dest = path.join('public/uploads', req.file.originalname);
fs.renameSync(req.file.path, dest);
res.json({ url: `/uploads/${req.file.originalname}` });
});
// Attacker uploads shell.php → /uploads/shell.php
// Full server compromiseClient-Side Only Validation
<!-- VULNERABLE: Only client-side validation -->
<input type="file" accept=".jpg,.png,.gif" />
<script>
function validateFile(input) {
const file = input.files[0];
const validTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!validTypes.includes(file.type)) {
alert('Only images allowed!');
return false;
}
return true;
}
</script>
<!-- Attacker bypasses using curl/Burp Suite:
curl -X POST -F "file=@shell.php" https://site.com/upload -->Secure Implementation
1. Validate File Type Properly
import fileType from 'file-type';
import path from 'path';
const ALLOWED_TYPES = {
'image/jpeg': ['.jpg', '.jpeg'],
'image/png': ['.png'],
'image/gif': ['.gif'],
'application/pdf': ['.pdf']
};
async function validateUpload(file) {
// 1. Check file size
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
if (file.size > MAX_SIZE) {
throw new Error('File too large');
}
// 2. Detect actual file type from content (magic bytes)
const detected = await fileType.fromBuffer(file.buffer);
if (!detected || !ALLOWED_TYPES[detected.mime]) {
throw new Error('Invalid file type');
}
// 3. Verify extension matches detected type
const ext = path.extname(file.originalname).toLowerCase();
if (!ALLOWED_TYPES[detected.mime].includes(ext)) {
throw new Error('Extension does not match file type');
}
return true;
}2. Rename and Store Safely
import crypto from 'crypto';
import path from 'path';
async function saveUpload(file, userId) {
// Generate random filename - never use original
const randomName = crypto.randomBytes(16).toString('hex');
const ext = path.extname(file.originalname).toLowerCase();
const filename = `${randomName}${ext}`;
// Store outside web root or use cloud storage
const storagePath = path.join(
process.env.UPLOAD_DIR, // Not in public folder!
userId.toString(),
filename
);
await fs.promises.writeFile(storagePath, file.buffer);
// Return reference ID, not file path
return await db.files.create({
userId,
filename,
originalName: file.originalname,
mimeType: file.mimetype,
size: file.size
});
}3. Serve Files Safely
// Serve files through authenticated endpoint
app.get('/files/:id', authenticate, async (req, res) => {
const file = await db.files.findById(req.params.id);
if (!file || file.userId !== req.session.userId) {
return res.status(404).send('Not found');
}
const filePath = path.join(process.env.UPLOAD_DIR, file.filename);
// Set secure headers
res.set({
'Content-Type': file.mimeType,
'Content-Disposition': `attachment; filename="${file.originalName}"`,
'X-Content-Type-Options': 'nosniff', // Prevent MIME sniffing
'Content-Security-Policy': "default-src 'none'"
});
res.sendFile(filePath);
});4. Use Cloud Storage
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
async function uploadToS3(file, userId) {
const key = `uploads/${userId}/${crypto.randomUUID()}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
// Set Content-Disposition to force download
ContentDisposition: 'attachment'
}));
// Return signed URL for access (expires in 1 hour)
return getSignedUrl(s3, new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key
}), { expiresIn: 3600 });
}Security Checklist
- Validate file type by content (magic bytes), not extension
- Use allowlist of permitted file types
- Generate random filenames, never use original
- Store uploads outside web root
- Serve files with Content-Disposition: attachment
- Set X-Content-Type-Options: nosniff
- Enforce maximum file size limits
- Scan uploads with antivirus when possible
- Use cloud storage with signed URLs for sensitive files
Practice Challenges
View allUpload Extension Bypass
File upload checks extension. But not well enough.
Upload Content-Type
Content-Type validation only. Easy to bypass.
Upload SVG XSS
SVG uploads allowed. SVG can contain JavaScript.
Upload Polyglot
Create a file that's both an image and a script.
Upload Race
File is uploaded then validated. Race condition.