Secure File Upload Implementation: Validation, Storage, and Scan Patterns

File upload functionality is one of the most frequently exploited features in web applications. An insecure upload endpoint can lead to remote code execution, malware distribution, data exfiltration, and server compromise. Every upload must be treated as a potential attack.
Multi-Layer File Validation
Relying on a single validation technique — such as checking the Content-Type header — is insufficient because HTTP headers can be spoofed. A defense-in-depth approach validates at multiple layers.
import { fileTypeFromBuffer } from 'file-type';
import crypto from 'crypto';
interface FileValidationResult {
valid: boolean;
mimeType: string;
extension: string;
error?: string;
}
async function validateUpload(
fileBuffer: Buffer,
originalName: string
): Promise<FileValidationResult> {
// Layer 1: File size check
const MAX_SIZE = 15 * 1024 * 1024; // 15 MB
if (fileBuffer.length > MAX_SIZE) {
return { valid: false, mimeType: '', extension: '', error: 'File too large' };
}
// Layer 2: Magic bytes verification (ignores Content-Type header)
const fileType = await fileTypeFromBuffer(fileBuffer);
if (!fileType) {
return { valid: false, mimeType: '', extension: '', error: 'Unknown file type' };
}
const ALLOWED_TYPES = new Map([
['image/jpeg', ['.jpg', '.jpeg']],
['image/png', ['.png']],
['image/webp', ['.webp']],
['application/pdf', ['.pdf']],
['application/zip', ['.zip']],
['text/csv', ['.csv']],
]);
if (!ALLOWED_TYPES.has(fileType.mime)) {
return { valid: false, mimeType: '', extension: '', error: 'File type not allowed' };
}
// Layer 3: Extension match
const ext = path.extname(originalName).toLowerCase();
const validExts = ALLOWED_TYPES.get(fileType.mime)!;
if (!validExts.includes(ext)) {
return { valid: false, mimeType: '', extension: '', error: 'Extension mismatch' };
}
return { valid: true, mimeType: fileType.mime, extension: ext };
}
Magic byte verification reads the file's header bytes to determine the actual content type, which attackers cannot spoof. Combine this with extension whitelisting and reject files where the declared type, detected type, and extension disagree.
Preventing Path Traversal
Path traversal attacks allow an attacker to write files to arbitrary locations on the filesystem by including ../ sequences in the filename. Sanitize filenames strictly.
function sanitizeFilename(filename: string): string {
// Remove path separators and parent directory references
let safe = filename
.replace(/[^a-zA-Z0-9._-]/g, '_') // Replace unsafe chars
.replace(/\.{2,}/g, '.') // Collapse multiple dots
.replace(/^\.+/g, '') // Remove leading dots
.substring(0, 255); // Limit length
// Generate a UUID-based filename and store the original separately
return `${crypto.randomUUID()}_${safe}`;
}
// Store files outside the web root
const UPLOAD_DIR = path.resolve('/var/data/uploads');
const safeFilename = sanitizeFilename(originalName);
const fullPath = path.join(UPLOAD_DIR, safeFilename);
// Verify the resolved path is within the upload directory
if (!fullPath.startsWith(UPLOAD_DIR + path.sep)) {
throw new Error('Path traversal detected');
}
Never use user-supplied filenames for server-side paths. Generate a random filename on the server and store the original name only in the database alongside the file record. Use path.resolve() followed by a prefix check to prevent traversal even through encoding tricks.
Secure Storage Patterns
Files should be stored outside the web root so they are not directly accessible via URL. For applications using cloud storage, configure bucket policies carefully.
# AWS S3: Secure bucket for file uploads
resource "aws_s3_bucket" "uploads" {
bucket = "soninow-uploads"
}
# Block public access entirely — serve through signed URLs
resource "aws_s3_bucket_public_access_block" "uploads" {
bucket = aws_s3_bucket.uploads.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Pre-signed URL generation for secure downloads
resource "aws_iam_policy" "generate_presigned_url" {
policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Action = ["s3:GetObject"],
Resource = "${aws_s3_bucket.uploads.arn}/*",
}],
})
}
Serve uploaded files through a proxy endpoint that validates authentication before returning the file content. For S3, generate pre-signed URLs with short expiration windows (5-15 minutes). For local storage, use an internal endpoint that reads the file and streams it to authenticated users.
Malware Scanning
Every uploaded file should be scanned for malware before it becomes accessible. Integrate with antivirus scanners like ClamAV at the upload level.
import { createScanner } from 'clamscan';
const scanner = await createScanner({
clamdscan: {
socket: '/var/run/clamav/clamd.sock',
timeout: 30000,
},
});
async function scanFile(filePath: string): Promise<boolean> {
try {
const { isInfected, viruses } = await scanner.isInfected(filePath);
if (isInfected) {
await fs.unlink(filePath); // Delete infected file immediately
logger.warn('Malware detected', { filePath, viruses });
return false;
}
return true;
} catch (error) {
logger.error('Scan error — quarantining file', { filePath, error });
// Move to quarantine for manual review
await fs.rename(filePath, filePath + '.quarantine');
return false;
}
}
For cloud storage, use the provider's malware scanning integration — AWS GuardDuty Malware Protection for S3 or Google Cloud Security Command Center. Scan files asynchronously after upload and mark them as unavailable until the scan completes.
Image Processing and Re-encoding
For image uploads, reprocess the image server-side using a secure library. This strips EXIF metadata (which can contain GPS coordinates), removes hidden data, and prevents ImageMagick/GD-based attacks.
import sharp from 'sharp';
async function processImage(filePath: string, mimeType: string): Promise<Buffer> {
const image = sharp(filePath);
// Re-encode to safe format — strips metadata and potential payload
const output = await image
.resize(2048, 2048, { fit: 'inside', withoutEnlargement: true })
.withMetadata(false) // Strip all metadata
.toFormat(mimeType === 'image/png' ? 'png' : 'jpeg', {
quality: 85,
})
.toBuffer();
return output;
}
Re-encoding destroys any steganographic payloads or embedded EXIF data in the original file. Set maximum output dimensions to prevent denial-of-service through decompression bombs.
Content-Disposition and Serving
When serving uploaded files to users, set headers that prevent execution and enforce download behavior:
app.get('/files/:id', async (req, res) => {
const file = await FileModel.findById(req.params.id);
if (!file) return res.status(404).end();
const filePath = path.join(UPLOAD_DIR, file.storedName);
res.set({
'Content-Type': file.mimeType,
'Content-Disposition': `attachment; filename="${file.originalName}"`,
'Content-Length': file.size.toString(),
'X-Content-Type-Options': 'nosniff',
'Cache-Control': 'private, max-age=3600',
});
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
});
The Content-Disposition: attachment header forces download rather than in-browser rendering, preventing HTML files from executing scripts. The X-Content-Type-Options: nosniff header prevents MIME-type sniffing.
Upload Rate Limiting
File upload endpoints are targets for denial-of-service attacks through large file uploads. Implement rate limiting specifically for upload endpoints:
const uploadRateLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
message: 'Upload limit exceeded. Try again later.',
keyGenerator: (req) => req.user?.id || req.ip,
});
app.post('/api/upload', uploadRateLimiter, uploadMiddleware, uploadHandler);
Parallelize upload limits across server instances using a shared Redis store. Track total upload volume per user per day and enforce quotas that align with your application's legitimate usage patterns.
Secure file upload requires careful implementation across multiple layers of validation and storage. Our <a href="/services/web-development">web development services</a> include secure file handling as a standard component. Contact SoniNow to review your file upload implementation.
Related Insights

API Rate Limiting Strategies: Token Bucket, Leaky Bucket, and Sliding Window
A guide to implementing API rate limiting including token bucket, leaky bucket, sliding window, and distributed rate limiting with Redis for production APIs.

API Security Best Practices: Authentication, Rate Limiting, and Input Validation
Best practices for securing APIs including API key management, OAuth token validation, rate limiting, input sanitization, CORS configuration, and request signing.

Authentication Patterns in Modern Web Apps: JWT, OAuth, and Session Management
A guide to authentication patterns for web applications including JWT implementation, OAuth 2.0 flows, refresh tokens, session management, and secure storage.