gongxiangp2/lib/brand-image-validation.ts
chenpei 7326e273e1 fix: harden media upload and release safety
Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
2026-08-12 18:04:08 +08:00

75 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sharp from 'sharp'
export const MAX_BRAND_IMAGE_BYTES = 5 * 1024 * 1024
export const MAX_BRAND_IMAGE_PIXELS = 4_194_304
export const MAX_BRAND_IMAGE_DIMENSION = 4096
export type ValidatedBrandImage = {
extension: 'png' | 'jpg' | 'webp'
contentType: 'image/png' | 'image/jpeg' | 'image/webp'
width: number
height: number
}
const FORMAT_DETAILS: Record<string, Pick<ValidatedBrandImage, 'extension' | 'contentType'>> = {
png: { extension: 'png', contentType: 'image/png' },
jpeg: { extension: 'jpg', contentType: 'image/jpeg' },
webp: { extension: 'webp', contentType: 'image/webp' },
}
/**
* 品牌图属于高可见度系统配置,不能只看 magic bytes。
* 这里会真正解码整张图,阻止截断文件、伪造 MIME、动画和解压炸弹进入数据库。
*/
export async function validateBrandImage(
buffer: Buffer,
declaredContentType?: string | null,
): Promise<ValidatedBrandImage> {
if (buffer.length <= 0 || buffer.length > MAX_BRAND_IMAGE_BYTES) {
throw new Error('品牌图片必须是 5MB 以内的有效文件')
}
const image = sharp(buffer, {
failOn: 'error',
limitInputPixels: MAX_BRAND_IMAGE_PIXELS,
animated: false,
})
const metadata = await image.metadata()
const format = metadata.format ? FORMAT_DETAILS[metadata.format] : undefined
const width = metadata.width || 0
const height = metadata.height || 0
if (!format) {
throw new Error('品牌图片只支持 PNG、JPEG 或 WebP')
}
if (
width < 1 ||
height < 1 ||
width > MAX_BRAND_IMAGE_DIMENSION ||
height > MAX_BRAND_IMAGE_DIMENSION ||
width * height > MAX_BRAND_IMAGE_PIXELS
) {
throw new Error('品牌图片尺寸无效或像素过大')
}
if ((metadata.pages || 1) > 1) {
throw new Error('品牌图片不支持动画格式')
}
const normalizedDeclaredType = String(declaredContentType || '')
.split(';', 1)[0]
.trim()
.toLowerCase()
if (normalizedDeclaredType && normalizedDeclaredType !== format.contentType) {
throw new Error('品牌图片内容与声明格式不一致')
}
// metadata() 主要读取头信息;raw().toBuffer() 强制完整解码,截断图片会在这里失败。
await image.clone().raw().toBuffer()
return {
...format,
width,
height,
}
}