File upload has more security surface than most features: size limits, MIME type validation, filename sanitization, virus scanning, and storage management. Claude Code generates the full secure upload pipeline.
CLAUDE.md for File Upload Standards
## File Upload Rules
### Security (required)
- File size limits: images 10MB, documents 25MB, videos 500MB
- File type: validate both MIME type AND magic bytes
- Filename: replace with random UUID (never use original filename in path)
- Virus scanning: ClamAV or AWS S3 Malware Scanning
- Storage: S3 only (no local storage in production)
### Allowed file types
- Images: image/jpeg, image/png, image/webp, image/gif
- Documents: application/pdf
- Spreadsheets: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
### Upload flow
1. Client → API: send metadata → receive pre-signed URL
2. Client → S3: upload directly (bypass server)
3. S3 → API (webhook): upload completion notification
4. API: virus scan → DB registration → issue download URL
### Download URLs
- Pre-signed URL: expiry 1 hour
- No public ACL on sensitive files
Generating Pre-signed URL Upload Flow
Generate an S3 pre-signed URL upload design.
Flow:
1. API issues pre-signed URL
2. Client uploads directly to S3
3. S3 event triggers post-upload processing
4. API records file in DB
Requirements:
- File size limit enforced at URL issuance
- MIME type validation
- Replace filename with UUID
- Download URL expires in 1 hour
Generate:
- src/services/fileUploadService.ts
- src/routes/upload.ts
Generated Upload Service
// src/services/fileUploadService.ts
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { v4 as uuidv4 } from 'uuid';
const ALLOWED_MIME_TYPES = new Set([
'image/jpeg', 'image/png', 'image/webp', 'image/gif',
'application/pdf',
]);
const MAX_FILE_SIZES: Record<string, number> = {
'image/jpeg': 10 * 1024 * 1024,
'image/png': 10 * 1024 * 1024,
'image/webp': 10 * 1024 * 1024,
'image/gif': 10 * 1024 * 1024,
'application/pdf': 25 * 1024 * 1024,
};
export class FileUploadService {
private s3 = new S3Client({ region: process.env.AWS_REGION });
private bucket = process.env.S3_BUCKET!;
async createUploadUrl(
userId: string,
originalFilename: string,
mimeType: string,
fileSize: number
): Promise<{ uploadUrl: string; fileKey: string }> {
// MIME type validation
if (!ALLOWED_MIME_TYPES.has(mimeType)) {
throw new BadRequestError(`Unsupported file type: ${mimeType}`);
}
// File size validation
const maxSize = MAX_FILE_SIZES[mimeType];
if (fileSize > maxSize) {
throw new BadRequestError(
`File too large (max: ${maxSize / 1024 / 1024}MB for ${mimeType})`
);
}
// Generate safe path — never use original filename
const ext = originalFilename.split('.').pop()?.toLowerCase() ?? 'bin';
const fileKey = `uploads/${userId}/${uuidv4()}.${ext}`;
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: fileKey,
ContentType: mimeType,
// Store original name in metadata only (not in path)
Metadata: {
'original-filename': encodeURIComponent(originalFilename),
'uploaded-by': userId,
},
});
// 15-minute upload window
const uploadUrl = await getSignedUrl(this.s3, command, { expiresIn: 900 });
return { uploadUrl, fileKey };
}
async getDownloadUrl(fileKey: string): Promise<string> {
const command = new GetObjectCommand({ Bucket: this.bucket, Key: fileKey });
return getSignedUrl(this.s3, command, { expiresIn: 3600 }); // 1 hour
}
}
Magic Byte Validation
Generate a magic byte validator to prevent MIME type spoofing.
Validate actual file bytes, not just Content-Type header:
- JPEG, PNG, GIF, WebP, PDF
- Read only first few bytes (not entire file)
- Reject if bytes don't match claimed MIME type
Save to: src/utils/magicBytes.ts
// src/utils/magicBytes.ts
const MAGIC_BYTES: Record<string, number[][]> = {
'image/jpeg': [[0xff, 0xd8, 0xff]],
'image/png': [[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]],
'image/gif': [
[0x47, 0x49, 0x46, 0x38, 0x37], // GIF87a
[0x47, 0x49, 0x46, 0x38, 0x39], // GIF89a
],
'image/webp': [[0x52, 0x49, 0x46, 0x46]], // RIFF header
'application/pdf': [[0x25, 0x50, 0x44, 0x46]], // %PDF
};
export function validateMagicBytes(buffer: Buffer, mimeType: string): boolean {
const patterns = MAGIC_BYTES[mimeType];
if (!patterns) return false;
return patterns.some((pattern) =>
pattern.every((byte, index) => buffer[index] === byte)
);
}
Why magic bytes matter: a .php file renamed to .jpg passes MIME type checks but fails magic byte validation.
Image Resize Worker
Generate a BullMQ worker that resizes uploaded images to multiple sizes.
Trigger: S3 upload complete → add to BullMQ
Sizes: thumbnail (150x150), medium (800x600), original (unchanged)
Library: sharp
Output format: WebP (convert all inputs)
Output path: uploads/{userId}/{uuid}-{size}.webp
DB: save all size URLs to files table
Save to: src/workers/imageResizeWorker.ts
import sharp from 'sharp';
const SIZES = {
thumbnail: { width: 150, height: 150 },
medium: { width: 800, height: 600 },
};
const imageResizeWorker = new Worker('image-resize', async (job) => {
const { fileKey, userId, mimeType } = job.data;
if (!mimeType.startsWith('image/')) return;
// Download original from S3
const original = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: fileKey }));
const buffer = await streamToBuffer(original.Body);
const baseKey = fileKey.replace(/\.[^.]+$/, '');
const results: Record<string, string> = {};
for (const [sizeName, dimensions] of Object.entries(SIZES)) {
const resized = await sharp(buffer)
.resize(dimensions.width, dimensions.height, { fit: 'cover' })
.webp({ quality: 85 })
.toBuffer();
const sizeKey = `${baseKey}-${sizeName}.webp`;
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: sizeKey,
Body: resized,
ContentType: 'image/webp',
}));
results[sizeName] = sizeKey;
}
await prisma.file.update({
where: { s3Key: fileKey },
data: { thumbnailKey: results.thumbnail, mediumKey: results.medium },
});
});
Summary
Design secure file upload with Claude Code:
- CLAUDE.md — Size limits, allowed types, UUID filenames, no local storage
- Pre-signed URLs — Client uploads directly to S3, server never touches the bytes
- Magic byte validation — Prevent MIME type spoofing
- Image resize worker — BullMQ handles async resizing to multiple sizes
Security Pack (¥1,480) includes /security-check for file upload security — missing type validation, path traversal risks, local storage usage.
Myouga (@myougatheaxo) — Claude Code engineer focused on secure file handling.
Top comments (0)