/** * Server-side image normalisation. Single funnel for every image * upload (avatar, generic file, scan, attachment) to: * * - **Strip EXIF** (incl. GPS coords, device serial, photographer name) * so uploaded photos don't leak per-pixel PII to anyone with a * download URL. asset-auditor C1. * - **Cap dimensions** at MAX_DIMENSION so a 30000×30000 palette PNG * can't decompression-bomb a sharp decode further downstream. C2. * - **Re-encode via sharp** so polyglot trailing bytes (PDF+JPEG * sandwiches, HTML+PNG) are dropped - the output buffer is a clean * single-format file regardless of input trickery. H1. * - **Freeze animated GIFs** to first frame so a 5000-frame phishing * GIF can't pin a worker on every list-view render. H3. * * Falls through to the original buffer (with a warning at the call * site) when sharp isn't available or the input isn't a recognised * image. The MIME type stays the same as declared - magic-byte * verification has already run upstream. */ const MAX_DIMENSION = 4096; export async function normalizeImage( input: Buffer, mimeType: string, ): Promise<{ buffer: Buffer; format: string }> { const { default: sharp } = await import('sharp'); // Map MIME → sharp output format. Stay in-format so the stored // contentType keeps matching the bytes. const format = mimeFormat(mimeType); let pipeline = sharp(input, { animated: false, pages: 1 }) .rotate() // honour EXIF orientation BEFORE stripping it .resize({ width: MAX_DIMENSION, height: MAX_DIMENSION, fit: 'inside', withoutEnlargement: true, }) .withMetadata({ orientation: undefined }); // toFormat() drops anything non-format-shaped after re-encode. if (format === 'jpeg') pipeline = pipeline.jpeg({ quality: 88, mozjpeg: true }); else if (format === 'png') pipeline = pipeline.png({ compressionLevel: 9 }); else if (format === 'webp') pipeline = pipeline.webp({ quality: 88 }); else if (format === 'gif') pipeline = pipeline.gif(); const buffer = await pipeline.toBuffer(); return { buffer, format }; } function mimeFormat(mimeType: string): 'jpeg' | 'png' | 'webp' | 'gif' | 'unknown' { if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') return 'jpeg'; if (mimeType === 'image/png') return 'png'; if (mimeType === 'image/webp') return 'webp'; if (mimeType === 'image/gif') return 'gif'; return 'unknown'; }