mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 20:04:45 +00:00
fix(media): distrust image hints for container bytes
This commit is contained in:
@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. (#82443) Thanks @joshavant.
|
||||
- Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media.
|
||||
- Update/doctor: avoid materializing `groupAllowFrom` for channel schemas that reject it, so package-swap doctor repairs do not fail on externalized Slack configs.
|
||||
- Gateway/media: prevent image filenames from overriding generic non-image byte sniffing, so zip/octet-stream payloads mislabeled as images are offloaded or rejected before they become inline image attachments.
|
||||
- Plugins/web search: downgrade stale optional provider installs to warnings so Gateway and doctor repair paths keep running after startup provider selection. Refs #82313. Thanks @crackmac.
|
||||
|
||||
@@ -78,6 +78,30 @@ describe("mime detection", () => {
|
||||
},
|
||||
expected: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
},
|
||||
{
|
||||
name: "does not let image extensions override generic zip bytes",
|
||||
input: async () => {
|
||||
const zip = new JSZip();
|
||||
zip.file("hello.txt", "hi");
|
||||
return {
|
||||
buffer: await zip.generateAsync({ type: "nodebuffer" }),
|
||||
filePath: "/tmp/fake.png",
|
||||
};
|
||||
},
|
||||
expected: "application/zip",
|
||||
},
|
||||
{
|
||||
name: "does not let image headers override generic zip bytes",
|
||||
input: async () => {
|
||||
const zip = new JSZip();
|
||||
zip.file("hello.txt", "hi");
|
||||
return {
|
||||
buffer: await zip.generateAsync({ type: "nodebuffer" }),
|
||||
headerMime: "image/png",
|
||||
};
|
||||
},
|
||||
expected: "application/zip",
|
||||
},
|
||||
{
|
||||
name: "uses extension mapping for JavaScript assets",
|
||||
input: async () => ({
|
||||
|
||||
@@ -190,6 +190,10 @@ function isGenericMime(mime?: string): boolean {
|
||||
return m === "application/octet-stream" || m === "application/zip";
|
||||
}
|
||||
|
||||
function isImageMime(mime?: string): boolean {
|
||||
return mediaKindFromMime(normalizeMimeType(mime)) === "image";
|
||||
}
|
||||
|
||||
async function detectMimeImpl(opts: {
|
||||
buffer?: Buffer;
|
||||
headerMime?: string | null;
|
||||
@@ -200,23 +204,27 @@ async function detectMimeImpl(opts: {
|
||||
|
||||
const headerMime = normalizeMimeType(opts.headerMime);
|
||||
const sniffed = await sniffMime(opts.buffer);
|
||||
const sniffedGenericContainer = sniffed && isGenericMime(sniffed);
|
||||
const trustedExtMime = sniffedGenericContainer && isImageMime(extMime) ? undefined : extMime;
|
||||
const trustedHeaderMime =
|
||||
sniffedGenericContainer && isImageMime(headerMime) ? undefined : headerMime;
|
||||
|
||||
// Prefer sniffed types, but don't let generic container types override a more
|
||||
// specific extension mapping (e.g. XLSX vs ZIP).
|
||||
if (sniffed && (!isGenericMime(sniffed) || !extMime)) {
|
||||
if (sniffed && (!isGenericMime(sniffed) || !trustedExtMime)) {
|
||||
return sniffed;
|
||||
}
|
||||
if (extMime) {
|
||||
return extMime;
|
||||
if (trustedExtMime) {
|
||||
return trustedExtMime;
|
||||
}
|
||||
if (headerMime && !isGenericMime(headerMime)) {
|
||||
return headerMime;
|
||||
if (trustedHeaderMime && !isGenericMime(trustedHeaderMime)) {
|
||||
return trustedHeaderMime;
|
||||
}
|
||||
if (sniffed) {
|
||||
return sniffed;
|
||||
}
|
||||
if (headerMime) {
|
||||
return headerMime;
|
||||
if (trustedHeaderMime) {
|
||||
return trustedHeaderMime;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import JSZip from "jszip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
@@ -308,6 +309,19 @@ describe("loadWebMedia", () => {
|
||||
expect(result.buffer.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("does not treat image-named generic container bytes as local image media", async () => {
|
||||
const zip = new JSZip();
|
||||
zip.file("hello.txt", "hi");
|
||||
const fakeImage = path.join(fixtureRoot, "fake.png");
|
||||
await fs.writeFile(fakeImage, await zip.generateAsync({ type: "nodebuffer" }));
|
||||
|
||||
const result = await loadWebMedia(fakeImage, createLocalWebMediaOptions());
|
||||
|
||||
expect(result.kind).toBe("document");
|
||||
expect(result.contentType).toBe("application/zip");
|
||||
expect(result.fileName).toBe("fake.png");
|
||||
});
|
||||
|
||||
it("uses only the leaf filename from Windows-style sandbox-validated media paths", async () => {
|
||||
const result = await loadWebMedia(String.raw`C:\workspace\captures\tiny.png`, {
|
||||
maxBytes: 1024 * 1024,
|
||||
|
||||
Reference in New Issue
Block a user