File Upload Security
Secure file upload endpoints against malicious files, path traversal, resource exhaustion, and code execution
When to Use
- Building file upload endpoints (avatars, documents, attachments)
- Accepting user-generated content that includes files
- Storing uploaded files on disk or cloud storage
- Serving uploaded files back to users
- Reviewing existing upload functionality for security gaps
Instructions
- Validate file type by magic bytes, not just the extension or Content-Type header. Users can rename
malware.exetophoto.jpgand set any Content-Type. Inspect the file's magic bytes (first few bytes of the file content).
import { fileTypeFromBuffer } from 'file-type';
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'application/pdf']);
async function validateFileType(buffer: Buffer): Promise<string> {
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_TYPES.has(type.mime)) {
throw new ValidationError(`File type not allowed: ${type?.mime ?? 'unknown'}`);
}
return type.mime;
}
- Enforce file size limits. Prevent resource exhaustion by limiting both individual file size and total request size.
import multer from 'multer';
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB per file
files: 5, // Max 5 files per request
fieldSize: 1024, // Max 1 KB for text fields
},
storage: multer.memoryStorage(), // Store in memory for validation before saving
});
// Apply middleware
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file provided' });
const mime = await validateFileType(req.file.buffer);
// ... save file
});
- Generate random filenames. Never use the original filename — it can contain path traversal sequences (
../../etc/passwd) or special characters that break storage systems.
import crypto from 'node:crypto';
import path from 'node:path';
function generateSafeFilename(originalName: string): string {
const ext = path.extname(originalName).toLowerCase();
const allowedExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp', '.pdf']);
if (!allowedExtensions.has(ext)) {
throw new ValidationError(`Extension not allowed: ${ext}`);
}
return `${crypto.randomUUID()}${ext}`;
}
- Store uploads outside the web root. Never store uploaded files in a directory served by your web server. Use a dedicated storage directory or cloud storage (S3, GCS).
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
async function uploadToS3(buffer: Buffer, filename: string, contentType: string) {
const client = new S3Client({ region: 'us-east-1' });
await client.send(
new PutObjectCommand({
Bucket: process.env.UPLOAD_BUCKET,
Key: `uploads/${filename}`,
Body: buffer,
ContentType: contentType,
ContentDisposition: 'attachment', // Force download, prevent inline execution
})
);
}
- Serve files with
Content-Disposition: attachmentto prevent browsers from executing uploaded content (e.g., HTML files with JavaScript).
app.get('/files/:id', async (req, res) => {
const file = await getFileMetadata(req.params.id);
res.setHeader('Content-Type', file.mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${file.displayName}"`);
res.setHeader('X-Content-Type-Options', 'nosniff');
const stream = await getFileStream(file.storagePath);
stream.pipe(res);
});
Serve uploaded files from a separate domain. Use a different origin (e.g.,
uploads.example.com) so that any XSS in uploaded files cannot access cookies or APIs on your main domain.Scan uploads for malware in high-security environments. Use ClamAV or a cloud-based scanning service.
import NodeClam from 'clamscan';
const clam = await new NodeClam().init({
clamdscan: { socket: '/var/run/clamav/clamd.ctl' },
});
async function scanFile(filePath: string): Promise<boolean> {
const { isInfected } = await clam.isInfected(filePath);
return !isInfected;
}
- Strip metadata from images to remove EXIF data that may contain GPS coordinates, camera info, or embedded scripts.
import sharp from 'sharp';
async function sanitizeImage(buffer: Buffer): Promise<Buffer> {
return sharp(buffer)
.rotate() // Auto-rotate based on EXIF, then strip EXIF
.withMetadata(false) // Remove all metadata
.toBuffer();
}
- Implement upload rate limiting per user to prevent abuse.
import rateLimit from 'express-rate-limit';
const uploadLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // 20 uploads per window
message: { error: 'Too many uploads. Try again later.' },
keyGenerator: (req) => req.user?.id ?? req.ip,
});
app.post('/upload', uploadLimiter, upload.single('file'), handler);
- Log all upload activity with user ID, file hash, file type, and result (accepted/rejected) for audit trails.
Details
Attack vectors addressed:
- Malicious file execution: Attacker uploads a PHP/JSP shell disguised as an image. Mitigated by storing outside web root and generating random filenames.
- Path traversal: Filename contains
../../to write outside the upload directory. Mitigated by generating random filenames. - XSS via uploaded HTML/SVG: Attacker uploads an HTML file with JavaScript. Mitigated by
Content-Disposition: attachment,X-Content-Type-Options: nosniff, and serving from a separate domain. - Denial of service: Attacker uploads very large files or many files. Mitigated by size limits and rate limiting.
- Zip bombs: A small zip file that expands to gigabytes. If accepting archives, check uncompressed size before extraction.
Image-specific risks: SVG files can contain JavaScript (<script> tags, onload handlers). If accepting SVG uploads, sanitize them by parsing and removing script elements, or convert to a raster format.
Cloud storage best practices:
- Use pre-signed URLs for direct upload to S3/GCS (avoids your server handling large files)
- Set bucket policies to deny public access by default
- Enable server-side encryption
- Set lifecycle policies to auto-delete orphaned uploads
Common mistakes:
- Trusting the
Content-Typeheader (user-controlled) - Using the original filename for storage
- Serving uploads from the same origin as the application
- Not limiting file size at the server level (relying only on client-side limits)
- Storing uploads in a publicly accessible directory
Source
https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.