refactor(gateway): share Control UI bootstrap contract and CSP

This commit is contained in:
Peter Steinberger
2026-02-16 03:35:11 +01:00
parent 6e7c1c16e7
commit c6e6023e3a
4 changed files with 42 additions and 18 deletions

View File

@@ -0,0 +1,8 @@
export const CONTROL_UI_BOOTSTRAP_CONFIG_PATH = "/__openclaw/control-ui-config.json";
export type ControlUiBootstrapConfig = {
basePath: string;
assistantName: string;
assistantAvatar: string;
assistantAgentId: string;
};

View File

@@ -0,0 +1,12 @@
import { describe, expect, it } from "vitest";
import { buildControlUiCspHeader } from "./control-ui-csp.js";
describe("buildControlUiCspHeader", () => {
it("blocks inline scripts while allowing inline styles", () => {
const csp = buildControlUiCspHeader();
expect(csp).toContain("frame-ancestors 'none'");
expect(csp).toContain("script-src 'self'");
expect(csp).not.toContain("script-src 'self' 'unsafe-inline'");
expect(csp).toContain("style-src 'self' 'unsafe-inline'");
});
});

View File

@@ -0,0 +1,15 @@
export function buildControlUiCspHeader(): string {
// Control UI: block framing, block inline scripts, keep styles permissive
// (UI uses a lot of inline style attributes in templates).
return [
"default-src 'self'",
"base-uri 'none'",
"object-src 'none'",
"frame-ancestors 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' ws: wss:",
].join("; ");
}

View File

@@ -4,6 +4,11 @@ import path from "node:path";
import type { OpenClawConfig } from "../config/config.js";
import { resolveControlUiRootSync } from "../infra/control-ui-assets.js";
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.js";
import {
CONTROL_UI_BOOTSTRAP_CONFIG_PATH,
type ControlUiBootstrapConfig,
} from "./control-ui-contract.js";
import { buildControlUiCspHeader } from "./control-ui-csp.js";
import {
buildControlUiAvatarUrl,
CONTROL_UI_AVATAR_PREFIX,
@@ -12,7 +17,6 @@ import {
} from "./control-ui-shared.js";
const ROOT_PREFIX = "/";
const CONTROL_UI_BOOTSTRAP_CONFIG_PATH = "/__openclaw/control-ui-config.json";
export type ControlUiRequestOptions = {
basePath?: string;
@@ -69,22 +73,7 @@ type ControlUiAvatarMeta = {
function applyControlUiSecurityHeaders(res: ServerResponse) {
res.setHeader("X-Frame-Options", "DENY");
// Control UI: block framing, block inline scripts, keep styles permissive
// (UI uses a lot of inline style attributes in templates).
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
"base-uri 'none'",
"object-src 'none'",
"frame-ancestors 'none'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' ws: wss:",
].join("; "),
);
res.setHeader("Content-Security-Policy", buildControlUiCspHeader());
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("Referrer-Policy", "no-referrer");
}
@@ -265,7 +254,7 @@ export function handleControlUiHttpRequest(
assistantName: identity.name,
assistantAvatar: avatarValue ?? identity.avatar,
assistantAgentId: identity.agentId,
});
} satisfies ControlUiBootstrapConfig);
return true;
}