Skip to content

Commit 2ad70b0

Browse files
authored
Merge pull request #17 from yourclaw/fix/configurable-frame-ancestors
fix: configurable CSP frame-ancestors for dashboard embedding
2 parents 98edbe8 + ca5abc6 commit 2ad70b0

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

src/gateway/control-ui-csp.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import { describe, expect, it } from "vitest";
1+
import { afterEach, describe, expect, it } from "vitest";
22
import { buildControlUiCspHeader } from "./control-ui-csp.js";
33

44
describe("buildControlUiCspHeader", () => {
5+
const originalEnv = process.env.OPENCLAW_FRAME_ANCESTORS;
6+
7+
afterEach(() => {
8+
if (originalEnv === undefined) {
9+
delete process.env.OPENCLAW_FRAME_ANCESTORS;
10+
} else {
11+
process.env.OPENCLAW_FRAME_ANCESTORS = originalEnv;
12+
}
13+
});
14+
515
it("blocks inline scripts while allowing inline styles", () => {
16+
delete process.env.OPENCLAW_FRAME_ANCESTORS;
617
const csp = buildControlUiCspHeader();
718
expect(csp).toContain("frame-ancestors 'none'");
819
expect(csp).toContain("script-src 'self'");
@@ -15,4 +26,17 @@ describe("buildControlUiCspHeader", () => {
1526
expect(csp).toContain("https://fonts.googleapis.com");
1627
expect(csp).toContain("font-src 'self' https://fonts.gstatic.com");
1728
});
29+
30+
it("respects OPENCLAW_FRAME_ANCESTORS env var", () => {
31+
process.env.OPENCLAW_FRAME_ANCESTORS = "https://www.yourclaw.ai https://yourclaw.ai";
32+
const csp = buildControlUiCspHeader();
33+
expect(csp).toContain("frame-ancestors https://www.yourclaw.ai https://yourclaw.ai");
34+
expect(csp).not.toContain("frame-ancestors 'none'");
35+
});
36+
37+
it("defaults to frame-ancestors 'none' when env var is empty", () => {
38+
process.env.OPENCLAW_FRAME_ANCESTORS = "";
39+
const csp = buildControlUiCspHeader();
40+
expect(csp).toContain("frame-ancestors 'none'");
41+
});
1842
});

src/gateway/control-ui-csp.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
export function buildControlUiCspHeader(): string {
2-
// Control UI: block framing, block inline scripts, keep styles permissive
3-
// (UI uses a lot of inline style attributes in templates).
2+
// Control UI: block framing by default, block inline scripts, keep styles
3+
// permissive (UI uses a lot of inline style attributes in templates).
44
// Keep Google Fonts origins explicit in CSP for deployments that load
55
// external Google Fonts stylesheets/font files.
6+
//
7+
// OPENCLAW_FRAME_ANCESTORS: space-separated list of origins allowed to
8+
// embed this page in an iframe (e.g. "https://www.yourclaw.ai").
9+
// Defaults to 'none' (no embedding) when unset.
10+
const frameAncestors =
11+
process.env.OPENCLAW_FRAME_ANCESTORS?.trim() || "'none'";
612
return [
713
"default-src 'self'",
814
"base-uri 'none'",
915
"object-src 'none'",
10-
"frame-ancestors 'none'",
16+
`frame-ancestors ${frameAncestors}`,
1117
"script-src 'self'",
1218
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
1319
"img-src 'self' data: https:",

src/gateway/control-ui.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,12 @@ type ControlUiAvatarMeta = {
9898
};
9999

100100
function applyControlUiSecurityHeaders(res: ServerResponse) {
101-
res.setHeader("X-Frame-Options", "DENY");
101+
// Only set X-Frame-Options: DENY when framing is fully blocked.
102+
// When OPENCLAW_FRAME_ANCESTORS is configured, CSP frame-ancestors
103+
// takes precedence and X-Frame-Options would conflict.
104+
if (!process.env.OPENCLAW_FRAME_ANCESTORS?.trim()) {
105+
res.setHeader("X-Frame-Options", "DENY");
106+
}
102107
res.setHeader("Content-Security-Policy", buildControlUiCspHeader());
103108
res.setHeader("X-Content-Type-Options", "nosniff");
104109
res.setHeader("Referrer-Policy", "no-referrer");
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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

Comments
 (0)