mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 05:23:42 +00:00
fix(gateway): bound webchat image data scans
This commit is contained in:
@@ -297,6 +297,29 @@ describe("buildWebchatAssistantMessageFromReplyPayloads", () => {
|
||||
expect(message).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps valid whitespace in data image payloads", async () => {
|
||||
const imageUrl = "data:image/png;base64,cG 5n\n";
|
||||
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
||||
{
|
||||
text: "whitespace image",
|
||||
mediaUrl: imageUrl,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(message?.content).toContainEqual({ type: "input_image", image_url: imageUrl.trim() });
|
||||
});
|
||||
|
||||
it("rejects invalid data image payload characters", async () => {
|
||||
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
||||
{
|
||||
text: "invalid",
|
||||
mediaUrl: "data:image/png;base64,cG5n!",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(message).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects remote image URLs", async () => {
|
||||
const message = await buildWebchatAssistantMessageFromReplyPayloads([
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from "node:path";
|
||||
import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
|
||||
import { openLocalFileSafely } from "../../infra/fs-safe.js";
|
||||
import { assertNoWindowsNetworkPath, safeFileURLToPath } from "../../infra/local-file-access.js";
|
||||
import { estimateBase64DecodedBytes } from "../../media/base64.js";
|
||||
import { assertLocalMediaAllowed, LocalMediaAccessError } from "../../media/local-media-access.js";
|
||||
import { isAudioFileName } from "../../media/mime.js";
|
||||
import { resolveSendableOutboundReplyParts } from "../../plugin-sdk/reply-payload.js";
|
||||
@@ -131,10 +132,31 @@ function mimeTypeForPath(filePath: string): string {
|
||||
return MIME_BY_EXT[ext] ?? "audio/mpeg";
|
||||
}
|
||||
|
||||
function estimateBase64DecodedBytes(base64: string): number {
|
||||
const sanitized = base64.replace(/\s+/g, "");
|
||||
const padding = sanitized.endsWith("==") ? 2 : sanitized.endsWith("=") ? 1 : 0;
|
||||
return Math.floor((sanitized.length * 3) / 4) - padding;
|
||||
function isBase64DataPayload(value: string): boolean {
|
||||
if (value.length === 0) {
|
||||
return false;
|
||||
}
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index);
|
||||
const isBase64Char =
|
||||
(code >= 0x41 && code <= 0x5a) ||
|
||||
(code >= 0x61 && code <= 0x7a) ||
|
||||
(code >= 0x30 && code <= 0x39) ||
|
||||
code === 0x2b ||
|
||||
code === 0x2f ||
|
||||
code === 0x3d;
|
||||
const isWhitespace =
|
||||
code === 0x09 ||
|
||||
code === 0x0a ||
|
||||
code === 0x0b ||
|
||||
code === 0x0c ||
|
||||
code === 0x0d ||
|
||||
code === 0x20;
|
||||
if (!isBase64Char && !isWhitespace) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveEmbeddableImageUrl(url: string): string | null {
|
||||
@@ -145,12 +167,17 @@ function resolveEmbeddableImageUrl(url: string): string | null {
|
||||
if (trimmed.length > MAX_WEBCHAT_IMAGE_DATA_URL_CHARS) {
|
||||
return null;
|
||||
}
|
||||
const match = /^data:(image\/[a-z0-9.+-]+);base64,([a-z0-9+/=\s]+)$/i.exec(trimmed);
|
||||
if (!match) {
|
||||
const commaIndex = trimmed.indexOf(",");
|
||||
if (commaIndex < 0) {
|
||||
return null;
|
||||
}
|
||||
const metadata = trimmed.slice(0, commaIndex);
|
||||
const match = /^data:(image\/[a-z0-9.+-]+);base64$/i.exec(metadata);
|
||||
const base64Data = trimmed.slice(commaIndex + 1);
|
||||
if (!match || !isBase64DataPayload(base64Data)) {
|
||||
return null;
|
||||
}
|
||||
const mediaType = normalizeLowercaseStringOrEmpty(match[1]);
|
||||
const base64Data = match[2];
|
||||
if (!ALLOWED_WEBCHAT_DATA_IMAGE_MEDIA_TYPES.has(mediaType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user