mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 05:51:39 +00:00
fix(ui): prevent Zod eval under strict Content Security Policy (#113617)
Co-authored-by: Peter Steinberger <steipete@golden-gate.local>
This commit is contained in:
committed by
GitHub
parent
57e2f220de
commit
d2ff17acc3
@@ -13,6 +13,10 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<script>
|
||||
// Configure Zod before module evaluation so its JIT probe cannot violate strict CSP.
|
||||
globalThis.__zod_globalConfig ??= {};
|
||||
globalThis.__zod_globalConfig.jitless = true;
|
||||
|
||||
(function () {
|
||||
var THEMES = { claw: 1, knot: 1, dash: 1 };
|
||||
var MODES = { system: 1, light: 1, dark: 1 };
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Control UI tests cover startup under the Gateway's strict script policy.
|
||||
import { chromium, type Browser } from "playwright";
|
||||
import { chromium, firefox, type Browser, type BrowserContext } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildControlUiCspHeader,
|
||||
computeInlineScriptHashes,
|
||||
} from "../../../src/gateway/control-ui-csp.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
canRunPlaywrightChromium as canRunPlaywrightBrowser,
|
||||
installMockGateway,
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
startControlUiE2eServer,
|
||||
@@ -14,11 +14,25 @@ import {
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const chromiumAvailable = canRunPlaywrightBrowser(chromiumExecutablePath);
|
||||
const firefoxExecutablePath = firefox.executablePath();
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
|
||||
let browser: Browser;
|
||||
const browsers: Array<{ name: string; launch: () => Promise<Browser> }> = [
|
||||
{
|
||||
name: "Chromium",
|
||||
launch: () => chromium.launch({ executablePath: chromiumExecutablePath }),
|
||||
},
|
||||
];
|
||||
|
||||
if (canRunPlaywrightBrowser(firefoxExecutablePath)) {
|
||||
browsers.push({
|
||||
name: "Firefox",
|
||||
launch: () => firefox.launch({ executablePath: firefoxExecutablePath }),
|
||||
});
|
||||
}
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
|
||||
describeControlUiE2e("Control UI strict CSP E2E", () => {
|
||||
@@ -27,42 +41,43 @@ describeControlUiE2e("Control UI strict CSP E2E", () => {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
|
||||
}
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close();
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("starts without probing eval under the Gateway script policy", async () => {
|
||||
const context = await browser.newContext({ serviceWorkers: "block" });
|
||||
await context.addInitScript(() => {
|
||||
const violations: Array<{ blockedUri: string; effectiveDirective: string }> = [];
|
||||
Object.assign(globalThis, { __openclawCspViolations: violations });
|
||||
document.addEventListener("securitypolicyviolation", (event) => {
|
||||
violations.push({
|
||||
blockedUri: event.blockedURI,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
});
|
||||
});
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page);
|
||||
await page.route(server.baseUrl, async (route) => {
|
||||
const response = await route.fetch();
|
||||
const body = await response.text();
|
||||
const csp = buildControlUiCspHeader({
|
||||
inlineScriptHashes: computeInlineScriptHashes(body),
|
||||
});
|
||||
await route.fulfill({
|
||||
body,
|
||||
headers: { ...response.headers(), "content-security-policy": csp },
|
||||
response,
|
||||
});
|
||||
});
|
||||
it.each(browsers)("starts without probing eval in $name", async ({ launch }) => {
|
||||
const browser = await launch();
|
||||
let context: BrowserContext | undefined;
|
||||
|
||||
try {
|
||||
context = await browser.newContext({ serviceWorkers: "block" });
|
||||
await context.addInitScript(() => {
|
||||
const violations: Array<{ blockedUri: string; effectiveDirective: string }> = [];
|
||||
Object.assign(globalThis, { __openclawCspViolations: violations });
|
||||
document.addEventListener("securitypolicyviolation", (event) => {
|
||||
violations.push({
|
||||
blockedUri: event.blockedURI,
|
||||
effectiveDirective: event.effectiveDirective,
|
||||
});
|
||||
});
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page);
|
||||
await page.route(server.baseUrl, async (route) => {
|
||||
const response = await route.fetch();
|
||||
const body = await response.text();
|
||||
const csp = buildControlUiCspHeader({
|
||||
inlineScriptHashes: computeInlineScriptHashes(body),
|
||||
});
|
||||
await route.fulfill({
|
||||
body,
|
||||
headers: { ...response.headers(), "content-security-policy": csp },
|
||||
response,
|
||||
});
|
||||
});
|
||||
|
||||
const response = await page.goto(server.baseUrl);
|
||||
expect(response?.status()).toBe(200);
|
||||
const csp = response?.headers()["content-security-policy"];
|
||||
@@ -73,24 +88,31 @@ describeControlUiE2e("Control UI strict CSP E2E", () => {
|
||||
.locator(".agent-chat__composer-combobox textarea")
|
||||
.waitFor({ state: "visible", timeout: 10_000 });
|
||||
|
||||
const evalViolations = await page.evaluate(() => {
|
||||
const violations = (
|
||||
globalThis as typeof globalThis & {
|
||||
__openclawCspViolations?: Array<{
|
||||
blockedUri: string;
|
||||
effectiveDirective: string;
|
||||
}>;
|
||||
}
|
||||
)["__openclawCspViolations"];
|
||||
return (violations ?? []).filter(
|
||||
(violation) =>
|
||||
violation.blockedUri === "eval" &&
|
||||
violation.effectiveDirective.startsWith("script-src"),
|
||||
);
|
||||
const cspState = await page.evaluate(() => {
|
||||
const runtime = globalThis as typeof globalThis & {
|
||||
__openclawCspViolations?: Array<{
|
||||
blockedUri: string;
|
||||
effectiveDirective: string;
|
||||
}>;
|
||||
__zod_globalConfig?: { jitless?: boolean };
|
||||
};
|
||||
return {
|
||||
jitless: runtime["__zod_globalConfig"]?.jitless === true,
|
||||
evalViolations: (runtime["__openclawCspViolations"] ?? []).filter(
|
||||
(violation) =>
|
||||
violation.blockedUri === "eval" &&
|
||||
violation.effectiveDirective.startsWith("script-src"),
|
||||
),
|
||||
};
|
||||
});
|
||||
expect(evalViolations).toEqual([]);
|
||||
expect(cspState.jitless).toBe(true);
|
||||
expect(cspState.evalViolations).toEqual([]);
|
||||
} finally {
|
||||
await context.close();
|
||||
try {
|
||||
await context?.close();
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user