|
| 1 | +diff --git a/src/gateway/control-ui-csp.test.ts b/src/gateway/control-ui-csp.test.ts |
| 2 | +index 7e69d48e7..13bfbe705 100644 |
| 3 | +--- a/src/gateway/control-ui-csp.test.ts |
| 4 | ++++ b/src/gateway/control-ui-csp.test.ts |
| 5 | +@@ -1,8 +1,19 @@ |
| 6 | +-import { describe, expect, it } from "vitest"; |
| 7 | ++import { afterEach, describe, expect, it } from "vitest"; |
| 8 | + import { buildControlUiCspHeader } from "./control-ui-csp.js"; |
| 9 | + |
| 10 | + describe("buildControlUiCspHeader", () => { |
| 11 | ++ const originalEnv = process.env.OPENCLAW_FRAME_ANCESTORS; |
| 12 | ++ |
| 13 | ++ afterEach(() => { |
| 14 | ++ if (originalEnv === undefined) { |
| 15 | ++ delete process.env.OPENCLAW_FRAME_ANCESTORS; |
| 16 | ++ } else { |
| 17 | ++ process.env.OPENCLAW_FRAME_ANCESTORS = originalEnv; |
| 18 | ++ } |
| 19 | ++ }); |
| 20 | ++ |
| 21 | + it("blocks inline scripts while allowing inline styles", () => { |
| 22 | ++ delete process.env.OPENCLAW_FRAME_ANCESTORS; |
| 23 | + const csp = buildControlUiCspHeader(); |
| 24 | + expect(csp).toContain("frame-ancestors 'none'"); |
| 25 | + expect(csp).toContain("script-src 'self'"); |
| 26 | +@@ -15,4 +26,17 @@ describe("buildControlUiCspHeader", () => { |
| 27 | + expect(csp).toContain("https://fonts.googleapis.com"); |
| 28 | + expect(csp).toContain("font-src 'self' https://fonts.gstatic.com"); |
| 29 | + }); |
| 30 | ++ |
| 31 | ++ it("respects OPENCLAW_FRAME_ANCESTORS env var", () => { |
| 32 | ++ process.env.OPENCLAW_FRAME_ANCESTORS = "https://www.yourclaw.ai https://yourclaw.ai"; |
| 33 | ++ const csp = buildControlUiCspHeader(); |
| 34 | ++ expect(csp).toContain("frame-ancestors https://www.yourclaw.ai https://yourclaw.ai"); |
| 35 | ++ expect(csp).not.toContain("frame-ancestors 'none'"); |
| 36 | ++ }); |
| 37 | ++ |
| 38 | ++ it("defaults to frame-ancestors 'none' when env var is empty", () => { |
| 39 | ++ process.env.OPENCLAW_FRAME_ANCESTORS = ""; |
| 40 | ++ const csp = buildControlUiCspHeader(); |
| 41 | ++ expect(csp).toContain("frame-ancestors 'none'"); |
| 42 | ++ }); |
| 43 | + }); |
| 44 | +diff --git a/src/gateway/control-ui-csp.ts b/src/gateway/control-ui-csp.ts |
| 45 | +index 8a7b56f1e..a3845fc2d 100644 |
| 46 | +--- a/src/gateway/control-ui-csp.ts |
| 47 | ++++ b/src/gateway/control-ui-csp.ts |
| 48 | +@@ -1,13 +1,19 @@ |
| 49 | + export function buildControlUiCspHeader(): string { |
| 50 | +- // Control UI: block framing, block inline scripts, keep styles permissive |
| 51 | +- // (UI uses a lot of inline style attributes in templates). |
| 52 | ++ // Control UI: block framing by default, block inline scripts, keep styles |
| 53 | ++ // permissive (UI uses a lot of inline style attributes in templates). |
| 54 | + // Keep Google Fonts origins explicit in CSP for deployments that load |
| 55 | + // external Google Fonts stylesheets/font files. |
| 56 | ++ // |
| 57 | ++ // OPENCLAW_FRAME_ANCESTORS: space-separated list of origins allowed to |
| 58 | ++ // embed this page in an iframe (e.g. "https://www.yourclaw.ai"). |
| 59 | ++ // Defaults to 'none' (no embedding) when unset. |
| 60 | ++ const frameAncestors = |
| 61 | ++ process.env.OPENCLAW_FRAME_ANCESTORS?.trim() || "'none'"; |
| 62 | + return [ |
| 63 | + "default-src 'self'", |
| 64 | + "base-uri 'none'", |
| 65 | + "object-src 'none'", |
| 66 | +- "frame-ancestors 'none'", |
| 67 | ++ `frame-ancestors ${frameAncestors}`, |
| 68 | + "script-src 'self'", |
| 69 | + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", |
| 70 | + "img-src 'self' data: https:", |
| 71 | +diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts |
| 72 | +index e410eb23d..fed754421 100644 |
| 73 | +--- a/src/gateway/control-ui.ts |
| 74 | ++++ b/src/gateway/control-ui.ts |
| 75 | +@@ -98,7 +98,12 @@ type ControlUiAvatarMeta = { |
| 76 | + }; |
| 77 | + |
| 78 | + function applyControlUiSecurityHeaders(res: ServerResponse) { |
| 79 | +- res.setHeader("X-Frame-Options", "DENY"); |
| 80 | ++ // Only set X-Frame-Options: DENY when framing is fully blocked. |
| 81 | ++ // When OPENCLAW_FRAME_ANCESTORS is configured, CSP frame-ancestors |
| 82 | ++ // takes precedence and X-Frame-Options would conflict. |
| 83 | ++ if (!process.env.OPENCLAW_FRAME_ANCESTORS?.trim()) { |
| 84 | ++ res.setHeader("X-Frame-Options", "DENY"); |
| 85 | ++ } |
| 86 | + res.setHeader("Content-Security-Policy", buildControlUiCspHeader()); |
| 87 | + res.setHeader("X-Content-Type-Options", "nosniff"); |
| 88 | + res.setHeader("Referrer-Policy", "no-referrer"); |
0 commit comments