2a6c3a1222
- Add validateImageMagicBytes() to storage.ts reading first 12 bytes to verify JPEG/PNG/WebP/ICO signatures regardless of declared MIME type - Remove image/svg+xml from favicon upload whitelist (SVG can embed scripts) - Apply magic bytes check in product image and favicon upload endpoints
40 lines
1.5 KiB
TypeScript
40 lines
1.5 KiB
TypeScript
import { mkdir, writeFile, unlink } from 'fs/promises'
|
|
import path from 'path'
|
|
|
|
const UPLOAD_DIR = path.join(process.cwd(), 'public', 'uploads')
|
|
|
|
export async function saveImage(
|
|
productId: string,
|
|
buffer: Buffer,
|
|
originalFilename: string
|
|
): Promise<string> {
|
|
const dir = path.join(UPLOAD_DIR, productId)
|
|
await mkdir(dir, { recursive: true })
|
|
const safeName = originalFilename.replace(/[^a-z0-9._-]/gi, '_').toLowerCase()
|
|
const filename = `${Date.now()}-${safeName}`
|
|
await writeFile(path.join(dir, filename), buffer)
|
|
return `/uploads/${productId}/${filename}`
|
|
}
|
|
|
|
const IMAGE_MAGIC_BYTES: Record<string, (buf: Buffer) => boolean> = {
|
|
'image/jpeg': (b) => b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff,
|
|
'image/png': (b) => b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e && b[3] === 0x47,
|
|
'image/webp': (b) =>
|
|
b[0] === 0x52 && b[1] === 0x49 && b[2] === 0x46 && b[3] === 0x46 &&
|
|
b[8] === 0x57 && b[9] === 0x45 && b[10] === 0x42 && b[11] === 0x50,
|
|
'image/x-icon': (b) => b[0] === 0x00 && b[1] === 0x00 && b[2] === 0x01 && b[3] === 0x00,
|
|
}
|
|
|
|
export async function validateImageMagicBytes(file: File, declaredType: string): Promise<boolean> {
|
|
const checker = IMAGE_MAGIC_BYTES[declaredType]
|
|
if (!checker) return false
|
|
const arrayBuffer = await file.slice(0, 12).arrayBuffer()
|
|
const buf = Buffer.from(arrayBuffer)
|
|
return checker(buf)
|
|
}
|
|
|
|
export async function deleteImageFile(url: string): Promise<void> {
|
|
const filePath = path.join(process.cwd(), 'public', url)
|
|
await unlink(filePath)
|
|
}
|