Skip to content

Commit bc6d949

Browse files
committed
fix(files): bound the HEIF fallback decode input
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.
1 parent 5596640 commit bc6d949

2 files changed

Lines changed: 27 additions & 0 deletions

File tree

apps/sim/lib/uploads/server/heic.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ describe('isHeifContainer', () => {
7676
})
7777

7878
describe('transcodeHeicToJpeg', () => {
79+
it('refuses to decode above the input ceiling', async () => {
80+
// Uploads allow 100MB; without this bound a tenant could spend an unbounded
81+
// WASM decode on a single read.
82+
const oversized = Buffer.alloc(20 * 1024 * 1024 + 1)
83+
expect(await transcodeHeicToJpeg(oversized)).toBeNull()
84+
})
85+
7986
it('returns null for bytes libheif cannot decode', async () => {
8087
// Also proves the dynamic `heic-convert` import resolves at runtime, which no
8188
// amount of type-checking establishes for a lazily loaded WebAssembly module.

apps/sim/lib/uploads/server/heic.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ const HEIF_BRANDS = new Set([
2424
'avis',
2525
])
2626

27+
/**
28+
* Byte ceiling for a fallback decode. Uploads allow 100MB and the vision path runs
29+
* sharp with `limitInputPixels: false`, so without this a tenant could push an
30+
* arbitrarily large HEIF through a single-threaded WebAssembly decode. 20MB leaves
31+
* generous headroom over any phone photo — a 12MP iPhone HEIC is 1-4MB — while
32+
* bounding what one read can cost.
33+
*
34+
* This bounds file size, not pixel count. A small file declaring enormous
35+
* dimensions is rejected during parse by libheif's own security limits.
36+
*/
37+
const MAX_TRANSCODE_INPUT_BYTES = 20 * 1024 * 1024
38+
2739
/**
2840
* Whether these bytes are an ISO-BMFF container in the HEIF family.
2941
*
@@ -58,6 +70,14 @@ export function isHeifContainer(buffer: Buffer): boolean {
5870
* Returns `null` when the bytes cannot be decoded; never a partial image.
5971
*/
6072
export async function transcodeHeicToJpeg(buffer: Buffer): Promise<Buffer | null> {
73+
if (buffer.length > MAX_TRANSCODE_INPUT_BYTES) {
74+
logger.warn('Skipped HEIC transcode above the input ceiling', {
75+
bytes: buffer.length,
76+
ceiling: MAX_TRANSCODE_INPUT_BYTES,
77+
})
78+
return null
79+
}
80+
6181
try {
6282
const convert = (await import('heic-convert')).default
6383
const jpeg = await convert({ buffer, format: 'JPEG' })

0 commit comments

Comments
 (0)