mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 19:14:58 +00:00
refactor(gateway): share Control UI bootstrap contract and CSP
This commit is contained in:
8
src/gateway/control-ui-contract.ts
Normal file
8
src/gateway/control-ui-contract.ts
Normal 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;
|
||||||
|
};
|
||||||
12
src/gateway/control-ui-csp.test.ts
Normal file
12
src/gateway/control-ui-csp.test.ts
Normal 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'");
|
||||||
|
});
|
||||||
|
});
|
||||||
15
src/gateway/control-ui-csp.ts
Normal file
15
src/gateway/control-ui-csp.ts
Normal 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("; ");
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ import path from "node:path";
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveControlUiRootSync } from "../infra/control-ui-assets.js";
|
import { resolveControlUiRootSync } from "../infra/control-ui-assets.js";
|
||||||
import { DEFAULT_ASSISTANT_IDENTITY, resolveAssistantIdentity } from "./assistant-identity.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 {
|
import {
|
||||||
buildControlUiAvatarUrl,
|
buildControlUiAvatarUrl,
|
||||||
CONTROL_UI_AVATAR_PREFIX,
|
CONTROL_UI_AVATAR_PREFIX,
|
||||||
@@ -12,7 +17,6 @@ import {
|
|||||||
} from "./control-ui-shared.js";
|
} from "./control-ui-shared.js";
|
||||||
|
|
||||||
const ROOT_PREFIX = "/";
|
const ROOT_PREFIX = "/";
|
||||||
const CONTROL_UI_BOOTSTRAP_CONFIG_PATH = "/__openclaw/control-ui-config.json";
|
|
||||||
|
|
||||||
export type ControlUiRequestOptions = {
|
export type ControlUiRequestOptions = {
|
||||||
basePath?: string;
|
basePath?: string;
|
||||||
@@ -69,22 +73,7 @@ type ControlUiAvatarMeta = {
|
|||||||
|
|
||||||
function applyControlUiSecurityHeaders(res: ServerResponse) {
|
function applyControlUiSecurityHeaders(res: ServerResponse) {
|
||||||
res.setHeader("X-Frame-Options", "DENY");
|
res.setHeader("X-Frame-Options", "DENY");
|
||||||
// Control UI: block framing, block inline scripts, keep styles permissive
|
res.setHeader("Content-Security-Policy", buildControlUiCspHeader());
|
||||||
// (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("X-Content-Type-Options", "nosniff");
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
||||||
res.setHeader("Referrer-Policy", "no-referrer");
|
res.setHeader("Referrer-Policy", "no-referrer");
|
||||||
}
|
}
|
||||||
@@ -265,7 +254,7 @@ export function handleControlUiHttpRequest(
|
|||||||
assistantName: identity.name,
|
assistantName: identity.name,
|
||||||
assistantAvatar: avatarValue ?? identity.avatar,
|
assistantAvatar: avatarValue ?? identity.avatar,
|
||||||
assistantAgentId: identity.agentId,
|
assistantAgentId: identity.agentId,
|
||||||
});
|
} satisfies ControlUiBootstrapConfig);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user