mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
ui(chat): allowlist image open URLs
This commit is contained in:
committed by
Peter Steinberger
parent
370d115549
commit
ebb5680893
@@ -5,6 +5,7 @@ import { toSanitizedMarkdownHtml } from "../markdown.ts";
|
||||
import { detectTextDirection } from "../text-direction.ts";
|
||||
import type { MessageGroup } from "../types/chat-types.ts";
|
||||
import { renderCopyAsMarkdownButton } from "./copy-as-markdown.ts";
|
||||
import { resolveSafeImageOpenUrl } from "./image-open.ts";
|
||||
import {
|
||||
extractTextCached,
|
||||
extractThinkingCached,
|
||||
@@ -201,7 +202,12 @@ function renderMessageImages(images: ImageBlock[]) {
|
||||
}
|
||||
|
||||
const openImage = (url: string) => {
|
||||
const opened = window.open(url, "_blank", "noopener,noreferrer");
|
||||
const safeUrl = resolveSafeImageOpenUrl(url, window.location.href);
|
||||
if (!safeUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const opened = window.open(safeUrl, "_blank", "noopener,noreferrer");
|
||||
if (opened) {
|
||||
opened.opener = null;
|
||||
}
|
||||
|
||||
48
ui/src/ui/chat/image-open.test.ts
Normal file
48
ui/src/ui/chat/image-open.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSafeImageOpenUrl } from "./image-open.ts";
|
||||
|
||||
describe("resolveSafeImageOpenUrl", () => {
|
||||
const baseHref = "https://openclaw.ai/chat";
|
||||
|
||||
it("allows absolute https URLs", () => {
|
||||
expect(resolveSafeImageOpenUrl("https://example.com/a.png?x=1#y", baseHref)).toBe(
|
||||
"https://example.com/a.png?x=1#y",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows relative URLs resolved against the current origin", () => {
|
||||
expect(resolveSafeImageOpenUrl("/assets/pic.png", baseHref)).toBe(
|
||||
"https://openclaw.ai/assets/pic.png",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows blob URLs", () => {
|
||||
expect(resolveSafeImageOpenUrl("blob:https://openclaw.ai/abc-123", baseHref)).toBe(
|
||||
"blob:https://openclaw.ai/abc-123",
|
||||
);
|
||||
});
|
||||
|
||||
it("allows data image URLs", () => {
|
||||
expect(resolveSafeImageOpenUrl("data:image/png;base64,iVBORw0KGgo=", baseHref)).toBe(
|
||||
"data:image/png;base64,iVBORw0KGgo=",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-image data URLs", () => {
|
||||
expect(
|
||||
resolveSafeImageOpenUrl("data:text/html,<script>alert(1)</script>", baseHref),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects javascript URLs", () => {
|
||||
expect(resolveSafeImageOpenUrl("javascript:alert(1)", baseHref)).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects file URLs", () => {
|
||||
expect(resolveSafeImageOpenUrl("file:///tmp/x.png", baseHref)).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects empty values", () => {
|
||||
expect(resolveSafeImageOpenUrl(" ", baseHref)).toBeNull();
|
||||
});
|
||||
});
|
||||
39
ui/src/ui/chat/image-open.ts
Normal file
39
ui/src/ui/chat/image-open.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
const DATA_URL_PREFIX = "data:";
|
||||
const ALLOWED_OPEN_PROTOCOLS = new Set(["http:", "https:", "blob:"]);
|
||||
|
||||
function isAllowedDataImageUrl(url: string): boolean {
|
||||
if (!url.toLowerCase().startsWith(DATA_URL_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commaIndex = url.indexOf(",");
|
||||
if (commaIndex < DATA_URL_PREFIX.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const metadata = url.slice(DATA_URL_PREFIX.length, commaIndex);
|
||||
const mimeType = metadata.split(";")[0]?.trim().toLowerCase() ?? "";
|
||||
return mimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
export function resolveSafeImageOpenUrl(rawUrl: string, baseHref: string): string | null {
|
||||
const candidate = rawUrl.trim();
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAllowedDataImageUrl(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
if (candidate.toLowerCase().startsWith(DATA_URL_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(candidate, baseHref);
|
||||
return ALLOWED_OPEN_PROTOCOLS.has(parsed.protocol.toLowerCase()) ? parsed.toString() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user