Skip to content

Commit 1508a58

Browse files
fix(media): case-sensitive Content-Type checks bypass header extension and image-header safety rules (#111184)
* fix(media): normalize Content-Type casing in header extension and image-header checks * test(media): consolidate MIME case coverage Co-authored-by: 潘晓波0668000512 <[email protected]> --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent ce9e393 commit 1508a58

2 files changed

Lines changed: 7 additions & 8 deletions

File tree

src/media/store.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -458,12 +458,12 @@ describe("media store", () => {
458458
},
459459
},
460460
{
461-
name: "prefers detected stream mime over generic zip header extension",
461+
name: "prefers detected stream mime over mixed-case generic zip header extension",
462462
run: async () => {
463463
await withTempStore(async (storeLocal10) => {
464464
const saved = await storeLocal10.saveMediaStream(
465465
Readable.from([Buffer.from("docx")]),
466-
"application/zip",
466+
"Application/Zip",
467467
"stream-inbound",
468468
1024,
469469
undefined,
@@ -687,13 +687,13 @@ describe("media store", () => {
687687
expectedExtension: ".custom",
688688
},
689689
{
690-
name: "does not preserve image header extensions for generic container buffers",
690+
name: "does not preserve mixed-case image header extensions for generic container buffers",
691691
bufferFactory: async () => {
692692
const zip = new JSZip();
693693
zip.file("hello.txt", "hi");
694694
return await zip.generateAsync({ type: "nodebuffer" });
695695
},
696-
contentType: "image/png",
696+
contentType: "IMAGE/PNG",
697697
originalFilename: "fake.png",
698698
expectedContentType: "application/zip",
699699
expectedExtension: ".zip",

src/media/store.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import {
99
extnameFromAnyPath,
1010
nameFromAnyPath,
1111
} from "@openclaw/media-core/file-name";
12-
import { detectMime, extensionForMime } from "@openclaw/media-core/mime";
12+
import { detectMime, extensionForMime, normalizeMimeType } from "@openclaw/media-core/mime";
1313
import { hasHttpUrlPrefix } from "@openclaw/net-policy/url-protocol";
14-
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
1514
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
1615
import { fileStore } from "../infra/file-store.js";
1716
import { sanitizeUntrustedFileName } from "../infra/fs-safe-advanced.js";
@@ -246,7 +245,7 @@ function safeOriginalFilenameExtension(originalFilename?: string): string | unde
246245
}
247246

248247
function extensionForAuthoritativeHeaderMime(contentType?: string): string | undefined {
249-
const mime = normalizeOptionalString(contentType?.split(";")[0]);
248+
const mime = normalizeMimeType(contentType);
250249
if (!mime || mime === "application/octet-stream" || mime === "binary/octet-stream") {
251250
return undefined;
252251
}
@@ -261,7 +260,7 @@ function isGenericContainerMime(mime?: string): boolean {
261260
}
262261

263262
function isImageHeaderMime(contentType?: string): boolean {
264-
return normalizeOptionalString(contentType?.split(";")[0])?.startsWith("image/") === true;
263+
return normalizeMimeType(contentType)?.startsWith("image/") === true;
265264
}
266265

267266
function resolveSavedMediaExtension(params: {

0 commit comments

Comments
 (0)