Skip to content

Commit a6cba75

Browse files
committed
security(media): anchor sanitizeMimeType regex to reject malformed input
1 parent a8a7012 commit a6cba75

2 files changed

Lines changed: 44 additions & 2 deletions

File tree

src/media-understanding/apply.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { MsgContext } from "../auto-reply/templating.js";
66
import type { OpenClawConfig } from "../config/types.js";
77
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
88
import { withEnvAsync } from "../test-utils/env.js";
9+
import { sanitizeMimeType } from "./apply.js";
910
import { createSafeAudioFixtureBuffer } from "./runner.test-utils.js";
1011
import type { MediaUnderstandingProvider } from "./types.js";
1112

@@ -1419,3 +1420,41 @@ describe("applyMediaUnderstanding", () => {
14191420
expect(ctx.Body).toContain("vendor-json");
14201421
});
14211422
});
1423+
1424+
describe("sanitizeMimeType", () => {
1425+
it("accepts a plain MIME type", () => {
1426+
expect(sanitizeMimeType("image/png")).toBe("image/png");
1427+
expect(sanitizeMimeType("application/vnd.api+json")).toBe("application/vnd.api+json");
1428+
});
1429+
1430+
it("strips standard parameters", () => {
1431+
expect(sanitizeMimeType("text/plain; charset=utf-8")).toBe("text/plain");
1432+
expect(sanitizeMimeType("text/csv;charset=UTF-8")).toBe("text/csv");
1433+
});
1434+
1435+
it("lowercases mixed-case input", () => {
1436+
expect(sanitizeMimeType("Image/PNG")).toBe("image/png");
1437+
});
1438+
1439+
it("returns undefined for empty or missing input", () => {
1440+
expect(sanitizeMimeType(undefined)).toBeUndefined();
1441+
expect(sanitizeMimeType("")).toBeUndefined();
1442+
expect(sanitizeMimeType(" ")).toBeUndefined();
1443+
});
1444+
1445+
it("rejects malformed input with trailing junk instead of truncating", () => {
1446+
expect(sanitizeMimeType("image/png junk")).toBeUndefined();
1447+
expect(sanitizeMimeType("image/png\nextra")).toBeUndefined();
1448+
});
1449+
1450+
it("rejects path-like inputs that previously captured an allowlisted prefix", () => {
1451+
expect(sanitizeMimeType("image/png/../etc/passwd")).toBeUndefined();
1452+
expect(sanitizeMimeType("image/png/evil")).toBeUndefined();
1453+
});
1454+
1455+
it("rejects inputs without a type/subtype separator", () => {
1456+
expect(sanitizeMimeType("imagepng")).toBeUndefined();
1457+
expect(sanitizeMimeType("/png")).toBeUndefined();
1458+
expect(sanitizeMimeType("image/")).toBeUndefined();
1459+
});
1460+
});

src/media-understanding/apply.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,15 @@ const TEXT_EXT_MIME = new Map<string, string>([
7474
[".xml", "application/xml"],
7575
]);
7676

77-
function sanitizeMimeType(value?: string): string | undefined {
77+
// End-anchored so malformed input ("image/png junk", "image/png..etc")
78+
// returns undefined instead of silently truncating to a prefix that could
79+
// then pass the downstream allowlist check.
80+
export function sanitizeMimeType(value?: string): string | undefined {
7881
const trimmed = normalizeOptionalLowercaseString(value);
7982
if (!trimmed) {
8083
return undefined;
8184
}
82-
const match = trimmed.match(/^([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)/);
85+
const match = trimmed.match(/^([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)(?:;.*)?$/);
8386
return match?.[1];
8487
}
8588

0 commit comments

Comments
 (0)