mirror of
https://github.com/simstudioai/sim.git
synced 2026-08-31 01:11:53 +08:00
fix(files): bound the HEIF fallback decode input (#6348)
Uploads allow 100MB and prepareImageForVision runs sharp with limitInputPixels: false, so nothing upstream capped what could reach the single-threaded WebAssembly decoder. A tenant-controlled file could therefore spend unbounded CPU and memory on one read. Cap the transcode input at 20MB — generous headroom over any phone photo, which runs 1-4MB. Pixel-dimension bombs stay bounded by libheif's own security limits during parse.
This commit is contained in:
@@ -76,6 +76,13 @@ describe('isHeifContainer', () => {
|
||||
})
|
||||
|
||||
describe('transcodeHeicToJpeg', () => {
|
||||
it('refuses to decode above the input ceiling', async () => {
|
||||
// Uploads allow 100MB; without this bound a tenant could spend an unbounded
|
||||
// WASM decode on a single read.
|
||||
const oversized = Buffer.alloc(20 * 1024 * 1024 + 1)
|
||||
expect(await transcodeHeicToJpeg(oversized)).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for bytes libheif cannot decode', async () => {
|
||||
// Also proves the dynamic `heic-convert` import resolves at runtime, which no
|
||||
// amount of type-checking establishes for a lazily loaded WebAssembly module.
|
||||
|
||||
@@ -24,6 +24,18 @@ const HEIF_BRANDS = new Set([
|
||||
'avis',
|
||||
])
|
||||
|
||||
/**
|
||||
* Byte ceiling for a fallback decode. Uploads allow 100MB and the vision path runs
|
||||
* sharp with `limitInputPixels: false`, so without this a tenant could push an
|
||||
* arbitrarily large HEIF through a single-threaded WebAssembly decode. 20MB leaves
|
||||
* generous headroom over any phone photo — a 12MP iPhone HEIC is 1-4MB — while
|
||||
* bounding what one read can cost.
|
||||
*
|
||||
* This bounds file size, not pixel count. A small file declaring enormous
|
||||
* dimensions is rejected during parse by libheif's own security limits.
|
||||
*/
|
||||
const MAX_TRANSCODE_INPUT_BYTES = 20 * 1024 * 1024
|
||||
|
||||
/**
|
||||
* Whether these bytes are an ISO-BMFF container in the HEIF family.
|
||||
*
|
||||
@@ -58,6 +70,14 @@ export function isHeifContainer(buffer: Buffer): boolean {
|
||||
* Returns `null` when the bytes cannot be decoded; never a partial image.
|
||||
*/
|
||||
export async function transcodeHeicToJpeg(buffer: Buffer): Promise<Buffer | null> {
|
||||
if (buffer.length > MAX_TRANSCODE_INPUT_BYTES) {
|
||||
logger.warn('Skipped HEIC transcode above the input ceiling', {
|
||||
bytes: buffer.length,
|
||||
ceiling: MAX_TRANSCODE_INPUT_BYTES,
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const convert = (await import('heic-convert')).default
|
||||
const jpeg = await convert({ buffer, format: 'JPEG' })
|
||||
|
||||
Reference in New Issue
Block a user