Secure File Upload Architecture: Direct S3 Presigned URLs & Magic Number Validation
Eliminating server bottlenecks via direct client-to-storage uploads, pre-signed URL generation, magic-byte sniffing, virus scanning, and asynchronous processing.
By Amr Samir• August 19, 2026• 1 min
Secure File Upload Architecture: Direct S3 Presigned URLs & Magic Number Validation
1. The Flawed Traditional Upload Flow
Traditional architectures handle file uploads by streaming multipart form data directly through the application server:
- 100 users upload 50MB files concurrently.
- 5GB of payload floods the application server RAM and Node.js event loop.
- Server exhausts memory, drops active connections, and crashes.
code
[ Client ] -- 50MB File --> [ App Server (Memory Spikes!) ] -- 50MB File --> [ S3 Bucket ]
2. Modern Direct-to-Storage Architecture (Presigned URLs)
code
1. Request Upload Ticket:
[ Client ] --- POST /api/uploads/presign (filename, mime, size) ---> [ App Server ]
[ Client ] <--- 200 OK (Presigned S3 URL + Token) ----------------- [ App Server ]
2. Direct Upload to Cloud:
[ Client ] === Direct HTTP PUT (50MB Binary) ===> [ AWS S3 / Cloudflare R2 ]
3. Asynchronous Confirmation & Quarantine Scan:
[ S3 Event / Webhook ] ---> [ Async Worker / Lambda (Magic Bytes & Antivirus Scan) ]
3. Validating True File Types: Magic Numbers vs Extension Spoofing
Attackers can easily rename an executable malware.exe to avatar.jpg to bypass simple file extension checks.
Production backends must inspect the magic bytes in the file header:
typescript
import { fileTypeFromBuffer } from 'file-type';
export async function validateImageHeader(buffer: Buffer): Promise<boolean> {
const type = await fileTypeFromBuffer(buffer);
if (!type) return false;
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
return allowedMimeTypes.includes(type.mime);
}
4. Generating Secure Presigned Upload URLs in Backend
typescript
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
@Injectable()
export class UploadService {
private s3 = new S3Client({ region: process.env.AWS_REGION });
async generatePresignedUpload(userId: string, filename: string, mimeType: string) {
const key = `uploads/${userId}/${crypto.randomUUID()}-${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME,
Key: key,
ContentType: mimeType,
});
const uploadUrl = await getSignedUrl(this.s3, command, { expiresIn: 300 }); // 5 minutes valid
return { uploadUrl, fileKey: key };
}
}
5. Security Checklist
- Restrict max upload size via S3 bucket policies and presigned conditions.
- Never serve user uploads directly from the application domain (use isolated CDN domains like
media.example.com). - Sanitize SVGs or convert user images to WebP to eliminate stored XSS vectors.
Recommended Posts
Related Projects

LiveDocs - Real-Time Collaborative Document Editor
A modern web-based document editing suite featuring real-time state synchronization, rich-text controls, and multi-template document generation.