File size: 1,619 Bytes
2329815
 
 
 
 
 
 
 
 
 
 
 
 
53fe1f0
 
2329815
53fe1f0
 
 
 
 
 
2329815
 
 
 
53fe1f0
2329815
 
 
 
 
53fe1f0
2329815
 
53fe1f0
2329815
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const ALLOWED_MIME_TYPES = [
  'image/jpeg',
  'image/png',
];

const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png'];

// Magic bytes (file signatures) for image validation
const MAGIC_BYTES: Record<string, Buffer[]> = {
  'image/jpeg': [Buffer.from([0xff, 0xd8, 0xff])],
  'image/png': [Buffer.from([0x89, 0x50, 0x4e, 0x47])],
};

const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB

export function validateImageFile(file: Express.Multer.File): { valid: boolean; error?: string } {
  // 1. Check file size
  if (file.size > MAX_FILE_SIZE) {
    return { valid: false, error: 'File size must be under 2MB' };
  }

  // 2. Check MIME type
  if (!ALLOWED_MIME_TYPES.includes(file.mimetype)) {
    return { valid: false, error: 'Only JPEG and PNG images are allowed' };
  }

  // 3. Check file extension
  const ext = file.originalname.split('.').pop()?.toLowerCase();
  if (!ext || !ALLOWED_EXTENSIONS.includes(ext)) {
    return { valid: false, error: 'Invalid file extension' };
  }

  // 4. Validate magic bytes (check first 512 bytes for polyglot detection)
  const expectedSignatures = MAGIC_BYTES[file.mimetype];
  if (expectedSignatures) {
    const fileHeader = file.buffer.subarray(0, 512);
    const isValid = expectedSignatures.some(sig => fileHeader.subarray(0, sig.length).equals(sig));

    if (!isValid) {
      return { valid: false, error: 'File content does not match its type' };
    }
  }

  return { valid: true };
}

export function getSafeExtension(mimeType: string): string {
  const map: Record<string, string> = {
    'image/jpeg': 'jpg',
    'image/png': 'png',
  };
  return map[mimeType] || 'jpg';
}