From 965adc85eb26d2725cb5b35ca9944010d9214489 Mon Sep 17 00:00:00 2001 From: zqxuii <96630940+zqxuii@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:14:30 +0000 Subject: [PATCH] fix(media): ignore inherited prototype keys in the MIME allow-list mediaTypeForUpload gated on `ALLOWED_TYPES[mime]` (a truthy bracket lookup), so a Content-Type of "constructor" or "__proto__" resolved to an inherited Object.prototype member and returned truthy. The media upload route accepts the file whenever this returns non-null, so a crafted Content-Type slipped any file (e.g. evil.html) past the mp4/webm/mov allow-list. Gate on Object.hasOwn; add tests. --- lib/media.ts | 5 ++++- tests/media-upload-type.test.mjs | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/media.ts b/lib/media.ts index 5236afa..87b59e0 100644 --- a/lib/media.ts +++ b/lib/media.ts @@ -25,7 +25,10 @@ const ALLOWED_EXT_TYPES: Record = Object.fromEntries( export function mediaTypeForUpload(name: string | undefined | null, type: string | undefined | null): string | null { const mime = String(type || "").trim().toLowerCase(); - if (ALLOWED_TYPES[mime]) return mime; + // Use hasOwn, not a truthy bracket lookup: `ALLOWED_TYPES["constructor"]` / + // `["__proto__"]` resolve to inherited Object.prototype members and are truthy, + // which would let a crafted Content-Type slip any file past the video allow-list. + if (Object.hasOwn(ALLOWED_TYPES, mime)) return mime; if (mime && mime !== "application/octet-stream") return null; const lowerName = String(name || "").toLowerCase(); diff --git a/tests/media-upload-type.test.mjs b/tests/media-upload-type.test.mjs index fc9a21c..ccac800 100644 --- a/tests/media-upload-type.test.mjs +++ b/tests/media-upload-type.test.mjs @@ -20,3 +20,13 @@ test("media upload type rejects empty or generic non-video names", () => { assert.equal(mediaTypeForUpload("payload", "application/octet-stream"), null); assert.equal(mediaTypeForUpload("clip.gif", "application/octet-stream"), null); }); + +test("media upload type ignores inherited Object.prototype keys as MIME types", () => { + // A crafted Content-Type must not resolve through the prototype chain and + // slip a non-video file past the allow-list. + assert.equal(mediaTypeForUpload("evil.html", "constructor"), null); + assert.equal(mediaTypeForUpload("evil.html", "__proto__"), null); + assert.equal(mediaTypeForUpload("evil.exe", "hasOwnProperty"), null); + // An explicit non-allowed type is rejected outright, even with a video name. + assert.equal(mediaTypeForUpload("clip.mp4", "constructor"), null); +});