Back to Blog
File UploadSecurityAWS S3Backend

File Upload Handling: Secure and Scalable Uploads

Handle file uploads securely. Learn validation, storage strategies, and patterns for processing uploaded files.

B
Bootspring Team
Engineering
February 27, 2026
5 min read

File uploads require careful handling for security and scalability. This guide covers patterns for safe, efficient file processing.

Basic Upload with Multer#

1import multer from 'multer'; 2import path from 'path'; 3 4// Configure storage 5const storage = multer.diskStorage({ 6 destination: './uploads', 7 filename: (req, file, cb) => { 8 const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); 9 cb(null, uniqueSuffix + path.extname(file.originalname)); 10 }, 11}); 12 13// File filter 14const fileFilter = (req, file, cb) => { 15 const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; 16 17 if (allowedTypes.includes(file.mimetype)) { 18 cb(null, true); 19 } else { 20 cb(new Error('Invalid file type'), false); 21 } 22}; 23 24const upload = multer({ 25 storage, 26 fileFilter, 27 limits: { 28 fileSize: 5 * 1024 * 1024, // 5MB 29 files: 5, // Max 5 files 30 }, 31}); 32 33// Routes 34app.post('/upload', upload.single('file'), (req, res) => { 35 res.json({ filename: req.file?.filename }); 36}); 37 38app.post('/upload-multiple', upload.array('files', 5), (req, res) => { 39 res.json({ files: req.files?.map(f => f.filename) }); 40});

Upload to S3#

1import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; 2import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 3import multer from 'multer'; 4 5const s3 = new S3Client({ region: process.env.AWS_REGION }); 6 7// Memory storage for S3 upload 8const upload = multer({ 9 storage: multer.memoryStorage(), 10 limits: { fileSize: 10 * 1024 * 1024 }, 11}); 12 13app.post('/upload', upload.single('file'), async (req, res) => { 14 if (!req.file) { 15 return res.status(400).json({ error: 'No file provided' }); 16 } 17 18 const key = `uploads/${Date.now()}-${req.file.originalname}`; 19 20 await s3.send(new PutObjectCommand({ 21 Bucket: process.env.S3_BUCKET, 22 Key: key, 23 Body: req.file.buffer, 24 ContentType: req.file.mimetype, 25 })); 26 27 res.json({ 28 key, 29 url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`, 30 }); 31});

Presigned URLs for Direct Upload#

1// Generate presigned URL for client-side upload 2app.post('/upload/presign', async (req, res) => { 3 const { filename, contentType } = req.body; 4 5 const key = `uploads/${Date.now()}-${filename}`; 6 7 const command = new PutObjectCommand({ 8 Bucket: process.env.S3_BUCKET, 9 Key: key, 10 ContentType: contentType, 11 }); 12 13 const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); 14 15 res.json({ 16 uploadUrl, 17 key, 18 expiresIn: 3600, 19 }); 20}); 21 22// Client-side direct upload 23async function uploadFile(file: File) { 24 // Get presigned URL 25 const { uploadUrl, key } = await fetch('/upload/presign', { 26 method: 'POST', 27 headers: { 'Content-Type': 'application/json' }, 28 body: JSON.stringify({ 29 filename: file.name, 30 contentType: file.type, 31 }), 32 }).then(r => r.json()); 33 34 // Upload directly to S3 35 await fetch(uploadUrl, { 36 method: 'PUT', 37 body: file, 38 headers: { 'Content-Type': file.type }, 39 }); 40 41 return key; 42}

File Validation#

1import fileType from 'file-type'; 2import sharp from 'sharp'; 3 4async function validateImage(buffer: Buffer): Promise<void> { 5 // Check magic bytes, not just extension 6 const type = await fileType.fromBuffer(buffer); 7 8 if (!type || !['image/jpeg', 'image/png', 'image/webp'].includes(type.mime)) { 9 throw new Error('Invalid image type'); 10 } 11 12 // Validate image dimensions 13 const metadata = await sharp(buffer).metadata(); 14 15 if (!metadata.width || !metadata.height) { 16 throw new Error('Invalid image'); 17 } 18 19 if (metadata.width > 4096 || metadata.height > 4096) { 20 throw new Error('Image too large (max 4096x4096)'); 21 } 22} 23 24// Virus scanning (using ClamAV) 25import NodeClam from 'clamscan'; 26 27const clam = await new NodeClam().init({ 28 clamdscan: { 29 host: 'localhost', 30 port: 3310, 31 }, 32}); 33 34async function scanFile(filePath: string): Promise<boolean> { 35 const { isInfected } = await clam.scanFile(filePath); 36 return !isInfected; 37}

Image Processing#

1import sharp from 'sharp'; 2 3async function processImage(buffer: Buffer): Promise<{ 4 original: Buffer; 5 thumbnail: Buffer; 6 medium: Buffer; 7}> { 8 const image = sharp(buffer); 9 10 const [original, thumbnail, medium] = await Promise.all([ 11 // Optimize original 12 image 13 .clone() 14 .webp({ quality: 85 }) 15 .toBuffer(), 16 17 // Create thumbnail 18 image 19 .clone() 20 .resize(150, 150, { fit: 'cover' }) 21 .webp({ quality: 80 }) 22 .toBuffer(), 23 24 // Create medium size 25 image 26 .clone() 27 .resize(800, 800, { fit: 'inside', withoutEnlargement: true }) 28 .webp({ quality: 85 }) 29 .toBuffer(), 30 ]); 31 32 return { original, thumbnail, medium }; 33} 34 35app.post('/upload/image', upload.single('image'), async (req, res) => { 36 const variants = await processImage(req.file.buffer); 37 38 // Upload all variants 39 const baseKey = `images/${Date.now()}`; 40 41 await Promise.all([ 42 uploadToS3(`${baseKey}/original.webp`, variants.original), 43 uploadToS3(`${baseKey}/thumbnail.webp`, variants.thumbnail), 44 uploadToS3(`${baseKey}/medium.webp`, variants.medium), 45 ]); 46 47 res.json({ 48 original: `${baseKey}/original.webp`, 49 thumbnail: `${baseKey}/thumbnail.webp`, 50 medium: `${baseKey}/medium.webp`, 51 }); 52});

Chunked Uploads#

1// Initialize multipart upload 2app.post('/upload/init', async (req, res) => { 3 const { filename, contentType } = req.body; 4 const key = `uploads/${Date.now()}-${filename}`; 5 6 const { UploadId } = await s3.send(new CreateMultipartUploadCommand({ 7 Bucket: process.env.S3_BUCKET, 8 Key: key, 9 ContentType: contentType, 10 })); 11 12 res.json({ uploadId: UploadId, key }); 13}); 14 15// Upload chunk 16app.post('/upload/chunk', async (req, res) => { 17 const { uploadId, key, partNumber } = req.body; 18 19 const command = new UploadPartCommand({ 20 Bucket: process.env.S3_BUCKET, 21 Key: key, 22 UploadId: uploadId, 23 PartNumber: partNumber, 24 }); 25 26 const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); 27 28 res.json({ presignedUrl }); 29}); 30 31// Complete upload 32app.post('/upload/complete', async (req, res) => { 33 const { uploadId, key, parts } = req.body; 34 35 await s3.send(new CompleteMultipartUploadCommand({ 36 Bucket: process.env.S3_BUCKET, 37 Key: key, 38 UploadId: uploadId, 39 MultipartUpload: { Parts: parts }, 40 })); 41 42 res.json({ key }); 43});

Frontend Upload with Progress#

1function FileUploader() { 2 const [progress, setProgress] = useState(0); 3 4 const uploadFile = async (file: File) => { 5 const { uploadUrl, key } = await getPresignedUrl(file); 6 7 await new Promise((resolve, reject) => { 8 const xhr = new XMLHttpRequest(); 9 10 xhr.upload.addEventListener('progress', (e) => { 11 if (e.lengthComputable) { 12 setProgress(Math.round((e.loaded / e.total) * 100)); 13 } 14 }); 15 16 xhr.addEventListener('load', () => { 17 if (xhr.status === 200) resolve(key); 18 else reject(new Error('Upload failed')); 19 }); 20 21 xhr.open('PUT', uploadUrl); 22 xhr.setRequestHeader('Content-Type', file.type); 23 xhr.send(file); 24 }); 25 }; 26 27 return ( 28 <div> 29 <input type="file" onChange={(e) => uploadFile(e.target.files[0])} /> 30 {progress > 0 && <ProgressBar value={progress} />} 31 </div> 32 ); 33}

Security Checklist#

  • ✅ Validate file type by magic bytes, not extension
  • ✅ Limit file size
  • ✅ Generate random filenames
  • ✅ Store files outside web root
  • ✅ Scan for viruses
  • ✅ Validate image dimensions
  • ✅ Set proper content-type headers
  • ✅ Use presigned URLs for direct upload

Conclusion#

File uploads require validation, secure storage, and efficient processing. Use presigned URLs for large files, validate thoroughly before processing, and store files in object storage like S3 for scalability.

Share this article

Help spread the word about Bootspring