From 390843c67945f3139665fbc1f383a9cb24fb5616 Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Wed, 8 Jul 2026 09:53:39 -0400 Subject: [PATCH] prevent ddos on img --- apps/img/src/img.app.ts | 12 ++++++++---- apps/img/src/test/integration/api.test.ts | 8 ++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/img/src/img.app.ts b/apps/img/src/img.app.ts index 5ca806f..d92527a 100644 --- a/apps/img/src/img.app.ts +++ b/apps/img/src/img.app.ts @@ -19,8 +19,12 @@ const FALLBACK_ASSET_PATH = '/DefaultProfileImage.jpg' */ const CACHE_CONTROL = 'public, max-age=31536000, immutable' -/** Upper bound on a requested output dimension; guards against abuse. */ -const MAX_DIMENSION = 4096 +/** + * Allowed output dimensions. Restricting resizes to a small fixed set caps the + * number of distinct variants an attacker can request, so they can't blow past + * the edge cache and force the (expensive) WASM resize on every hit. + */ +const ALLOWED_DIMENSIONS = new Set([128, 256, 512, 1024]) /** JPEG quality used when re-encoding a resized image. */ const RESIZE_JPEG_QUALITY = 90 @@ -33,11 +37,11 @@ interface Transform { cropSquare: boolean } -/** Parse a positive-integer dimension query param, or `undefined` if invalid/absent. */ +/** Parse a dimension query param, or `undefined` if absent or not an allowed size. */ function parseDimension(value: string | undefined): number | undefined { if (value === undefined) return undefined const n = Number(value) - if (!Number.isInteger(n) || n <= 0 || n > MAX_DIMENSION) return undefined + if (!Number.isInteger(n) || !ALLOWED_DIMENSIONS.has(n)) return undefined return n } diff --git a/apps/img/src/test/integration/api.test.ts b/apps/img/src/test/integration/api.test.ts index 6b6cb20..0beafda 100644 --- a/apps/img/src/test/integration/api.test.ts +++ b/apps/img/src/test/integration/api.test.ts @@ -196,6 +196,14 @@ describe('img endpoints', () => { expect(new Uint8Array(await res.arrayBuffer())).toEqual(full) }) + it('ignores a ?width outside the allowed sizes and serves the original', async () => { + const full = new Uint8Array(await (await SELF.fetch(`${ORIGIN}/RecCenter.jpg`)).arrayBuffer()) + // 300 isn't one of 128/256/512/1024, so it's rejected and the source served. + const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=300`) + expect(res.status).toBe(200) + expect(new Uint8Array(await res.arrayBuffer())).toEqual(full) + }) + it('signs the resized body with ?width and ?sig=p1', async () => { const res = await SELF.fetch(`${ORIGIN}/RecCenter.jpg?width=512&sig=p1`) expect(res.status).toBe(200)