mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:40:43 +00:00
fix(media): allow host-local CSV uploads via Slack (#63604)
CSV files (text/csv) were rejected by assertHostReadMediaAllowed because content sniffers report text/plain for CSV — CSV is structurally indistinguishable from plain text at the byte level. Fix: - Add text/csv to HOST_READ_ALLOWED_DOCUMENT_MIMES - Add a targeted exception: when sniffed MIME is text/plain AND the extension-derived MIME is text/csv, allow the upload. The text/plain sniff already confirms the content is valid UTF-8 text (not binary), so the .csv extension is sufficient to confirm operator intent. Binary data disguised as .csv is still rejected because its sniffed MIME will not be text/plain (e.g. a PNG file sniffs as image/png). Fixes #63604
This commit is contained in:
committed by
Frank Yang
parent
f49d9bcae9
commit
5735772de6
@@ -185,6 +185,35 @@ describe("loadWebMedia", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows host-read CSV files", async () => {
|
||||
const csvFile = path.join(fixtureRoot, "data.csv");
|
||||
await fs.writeFile(csvFile, "name,value\nfoo,1\nbar,2\n", "utf8");
|
||||
const result = await loadWebMedia(csvFile, {
|
||||
maxBytes: 1024 * 1024,
|
||||
localRoots: "any",
|
||||
readFile: async (filePath) => await fs.readFile(filePath),
|
||||
hostReadCapability: true,
|
||||
});
|
||||
expect(result.kind).toBe("document");
|
||||
expect(result.contentType).toBe("text/csv");
|
||||
});
|
||||
|
||||
it("rejects binary data disguised as a CSV file", async () => {
|
||||
const fakeCsv = path.join(fixtureRoot, "evil.csv");
|
||||
// Write a PNG header — binary, not text
|
||||
await fs.writeFile(fakeCsv, Buffer.from(TINY_PNG_BASE64, "base64"));
|
||||
await expect(
|
||||
loadWebMedia(fakeCsv, {
|
||||
maxBytes: 1024 * 1024,
|
||||
localRoots: "any",
|
||||
readFile: async (filePath) => await fs.readFile(filePath),
|
||||
hostReadCapability: true,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: "path-not-allowed",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects traversal-style canvas media paths before filesystem access", async () => {
|
||||
await expect(
|
||||
loadWebMedia(`${CANVAS_HOST_PATH}/documents/../collection.media/tiny.png`),
|
||||
|
||||
@@ -83,6 +83,7 @@ const HOST_READ_ALLOWED_DOCUMENT_MIMES = new Set([
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"text/csv",
|
||||
]);
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
@@ -133,6 +134,13 @@ function assertHostReadMediaAllowed(params: {
|
||||
return;
|
||||
}
|
||||
const normalizedMime = normalizeMimeType(params.contentType);
|
||||
// CSV exception: content sniffers report text/plain for CSV because CSV is structurally
|
||||
// indistinguishable from plain text at the byte level. Allow it when:
|
||||
// - The extension-derived MIME is text/csv (operator intent)
|
||||
// - The content sniffed as text/plain (confirming valid text, not binary data)
|
||||
if (sniffedMime === "text/plain" && normalizedMime === "text/csv") {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
params.kind === "document" &&
|
||||
normalizedMime &&
|
||||
|
||||
Reference in New Issue
Block a user