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 <pan.xiaobo@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
YangManBOBO
2026-07-19 21:53:13 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent ce9e393196
commit 1508a58674
2 changed files with 7 additions and 8 deletions
+4 -4
View File
@@ -458,12 +458,12 @@ describe("media store", () => {
},
},
{
name: "prefers detected stream mime over generic zip header extension",
name: "prefers detected stream mime over mixed-case generic zip header extension",
run: async () => {
await withTempStore(async (storeLocal10) => {
const saved = await storeLocal10.saveMediaStream(
Readable.from([Buffer.from("docx")]),
"application/zip",
"Application/Zip",
"stream-inbound",
1024,
undefined,
@@ -687,13 +687,13 @@ describe("media store", () => {
expectedExtension: ".custom",
},
{
name: "does not preserve image header extensions for generic container buffers",
name: "does not preserve mixed-case image header extensions for generic container buffers",
bufferFactory: async () => {
const zip = new JSZip();
zip.file("hello.txt", "hi");
return await zip.generateAsync({ type: "nodebuffer" });
},
contentType: "image/png",
contentType: "IMAGE/PNG",
originalFilename: "fake.png",
expectedContentType: "application/zip",
expectedExtension: ".zip",
+3 -4
View File
@@ -9,9 +9,8 @@ import {
extnameFromAnyPath,
nameFromAnyPath,
} from "@openclaw/media-core/file-name";
import { detectMime, extensionForMime } from "@openclaw/media-core/mime";
import { detectMime, extensionForMime, normalizeMimeType } from "@openclaw/media-core/mime";
import { hasHttpUrlPrefix } from "@openclaw/net-policy/url-protocol";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { fileStore } from "../infra/file-store.js";
import { sanitizeUntrustedFileName } from "../infra/fs-safe-advanced.js";
@@ -246,7 +245,7 @@ function safeOriginalFilenameExtension(originalFilename?: string): string | unde
}
function extensionForAuthoritativeHeaderMime(contentType?: string): string | undefined {
const mime = normalizeOptionalString(contentType?.split(";")[0]);
const mime = normalizeMimeType(contentType);
if (!mime || mime === "application/octet-stream" || mime === "binary/octet-stream") {
return undefined;
}
@@ -261,7 +260,7 @@ function isGenericContainerMime(mime?: string): boolean {
}
function isImageHeaderMime(contentType?: string): boolean {
return normalizeOptionalString(contentType?.split(";")[0])?.startsWith("image/") === true;
return normalizeMimeType(contentType)?.startsWith("image/") === true;
}
function resolveSavedMediaExtension(params: {