Back to Learning Center
criticalA04:2021CWE-434CWE-79CWE-94

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
<?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 server

Extension Bypass Techniques

text
# 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 code

Magic Bytes Bypass

php
// 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 PHP

SVG XSS Attack

xml
<!-- 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

javascript
// 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 compromise

Client-Side Only Validation

html
<!-- 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

javascript
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

javascript
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

javascript
// 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

javascript
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 all