Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/sim/lib/uploads/server/heic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/lib/uploads/server/heic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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' })
Expand Down
Loading