mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
ui: block svg data image opens and harden tests
This commit is contained in:
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -259,6 +259,9 @@ jobs:
|
||||
- name: Check types and lint and oxfmt
|
||||
run: pnpm check
|
||||
|
||||
- name: Enforce safe external URL opening policy
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
|
||||
# Report-only dead-code scans. Runs after scope detection and stores machine-readable
|
||||
# results as artifacts for later triage before we enable hard gates.
|
||||
# Temporarily disabled in CI while we process initial findings.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveSafeExternalUrl } from "./open-external-url.ts";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { openExternalUrlSafe, resolveSafeExternalUrl } from "./open-external-url.ts";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("resolveSafeExternalUrl", () => {
|
||||
const baseHref = "https://openclaw.ai/chat";
|
||||
@@ -38,6 +43,18 @@ describe("resolveSafeExternalUrl", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects SVG data image URLs", () => {
|
||||
expect(
|
||||
resolveSafeExternalUrl(
|
||||
"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' />",
|
||||
baseHref,
|
||||
{
|
||||
allowDataImage: true,
|
||||
},
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects data image URLs unless explicitly enabled", () => {
|
||||
expect(resolveSafeExternalUrl("data:image/png;base64,iVBORw0KGgo=", baseHref)).toBeNull();
|
||||
});
|
||||
@@ -54,3 +71,26 @@ describe("resolveSafeExternalUrl", () => {
|
||||
expect(resolveSafeExternalUrl(" ", baseHref)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("openExternalUrlSafe", () => {
|
||||
it("nulls opener when window.open returns a proxy-like object", () => {
|
||||
const openedLikeProxy = {
|
||||
opener: { postMessage: () => void 0 },
|
||||
} as unknown as WindowProxy;
|
||||
const openMock = vi.fn(() => openedLikeProxy);
|
||||
vi.stubGlobal("window", {
|
||||
location: { href: "https://openclaw.ai/chat" },
|
||||
open: openMock,
|
||||
} as unknown as Window & typeof globalThis);
|
||||
|
||||
const opened = openExternalUrlSafe("https://example.com/safe.png");
|
||||
|
||||
expect(openMock).toHaveBeenCalledWith(
|
||||
"https://example.com/safe.png",
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
expect(opened).toBe(openedLikeProxy);
|
||||
expect(openedLikeProxy.opener).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const DATA_URL_PREFIX = "data:";
|
||||
const ALLOWED_EXTERNAL_PROTOCOLS = new Set(["http:", "https:", "blob:"]);
|
||||
const BLOCKED_DATA_IMAGE_MIME_TYPES = new Set(["image/svg+xml"]);
|
||||
|
||||
function isAllowedDataImageUrl(url: string): boolean {
|
||||
if (!url.toLowerCase().startsWith(DATA_URL_PREFIX)) {
|
||||
@@ -13,7 +14,11 @@ function isAllowedDataImageUrl(url: string): boolean {
|
||||
|
||||
const metadata = url.slice(DATA_URL_PREFIX.length, commaIndex);
|
||||
const mimeType = metadata.split(";")[0]?.trim().toLowerCase() ?? "";
|
||||
return mimeType.startsWith("image/");
|
||||
if (!mimeType.startsWith("image/")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !BLOCKED_DATA_IMAGE_MIME_TYPES.has(mimeType);
|
||||
}
|
||||
|
||||
export type ResolveSafeExternalUrlOptions = {
|
||||
|
||||
@@ -50,4 +50,21 @@ describe("chat image open safety", () => {
|
||||
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not open SVG data image URLs", async () => {
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
|
||||
const openSpy = vi.spyOn(window, "open").mockReturnValue(null);
|
||||
app.chatMessages = [
|
||||
renderAssistantImage("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' />"),
|
||||
];
|
||||
await app.updateComplete;
|
||||
|
||||
const image = app.querySelector<HTMLImageElement>(".chat-message-image");
|
||||
expect(image).not.toBeNull();
|
||||
image?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(openSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user