🌿 Collaborative wiki on ATProto
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add more image validation

juprodh 1c7e6bb8 6f53008e

+30 -3
+30 -3
src/lib/image.ts
··· 20 20 return (ALLOWED_MIME_TYPES as readonly string[]).includes(mimeType); 21 21 } 22 22 23 + const SHARP_FORMAT_TO_MIME: Record<string, AllowedMimeType> = { 24 + jpeg: "image/jpeg", 25 + png: "image/png", 26 + gif: "image/gif", 27 + webp: "image/webp", 28 + }; 29 + 30 + /** GIF magic bytes: "GIF87a" or "GIF89a" */ 31 + function isGifData(data: Buffer): boolean { 32 + return data.length >= 6 && data.toString("ascii", 0, 3) === "GIF"; 33 + } 34 + 23 35 export async function processImage( 24 36 data: Buffer, 25 37 mimeType: string, ··· 37 49 ); 38 50 } 39 51 40 - // GIFs: pass through (sharp's animated GIF support is limited, GIFs rarely have EXIF) 52 + // GIFs: validate magic bytes, then pass through 53 + // (sharp's animated GIF support is limited, GIFs rarely have EXIF) 41 54 if (mimeType === "image/gif") { 55 + if (!isGifData(data)) { 56 + throw new ImageValidationError("File content is not a valid GIF"); 57 + } 42 58 return { data, mimeType }; 43 59 } 44 60 45 - // Auto-orient (applies EXIF rotation) and strip metadata 61 + // Auto-orient (applies EXIF rotation) and strip metadata. 62 + // sharp validates magic bytes internally -- throws on non-image data. 46 63 const { default: sharp } = await import("sharp"); 47 - const processed = await sharp(data).rotate().toBuffer(); 64 + const instance = sharp(data); 65 + const meta = await instance.metadata(); 66 + const detectedMime = meta.format 67 + ? SHARP_FORMAT_TO_MIME[meta.format] 68 + : undefined; 69 + if (detectedMime && detectedMime !== mimeType) { 70 + throw new ImageValidationError( 71 + `MIME type mismatch: claimed ${mimeType} but content is ${detectedMime}`, 72 + ); 73 + } 74 + const processed = await instance.rotate().toBuffer(); 48 75 return { data: processed, mimeType }; 49 76 }