Public: anyone can read this; the crew can change it.

A new app from the template sends Cross-Origin-Opener-Policy: same-origin, and the web spec check asks for it #3991

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3399-a-new-app-from-the-template-sends-cross → main 5 commits fork 1 thread · 1 unresolved

  • Merged. scout/3399-a-new-app-from-the-template-sends-cross was merged into main as 0b78bb6.

Review

Sign in to review this change.
4 files changed +19 −0
src/apps/scaffold.ts +5
@@ -150,6 +150,10 @@ bun run contract:emit # rewrite contract/opena
150150 it: the preview comes up the moment the app answers, and says \`live\`, or
151151 \`serving · tests failed\` the moment the suite disagrees.
152152
153 Every response carries \`Content-Security-Policy\`, \`X-Content-Type-Options\`,
154 \`Referrer-Policy\` and \`Cross-Origin-Opener-Policy: same-origin\` (the last
155 cuts a page off from any window that opened it); they are set in \`src/http.ts\`.
156
153157 ## The shape
154158
155159 One implementation, many doors. \`src/actions.ts\` is every operation as a
@@ -1376,6 +1380,7 @@ export const SECURITY: Record<string, string> = {
13761380 "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
13771381 "x-content-type-options": "nosniff",
13781382 "referrer-policy": "strict-origin-when-cross-origin",
1383 "cross-origin-opener-policy": "same-origin",

The comment above SECURITY explains each header but not this one. Add a clause saying COOP cuts the page off from any window that opened it.

13791384 "permissions-policy": "camera=(), microphone=(), geolocation=()",
13801385 ...(publicOrigin.startsWith("https:")
13811386 ? { "strict-transport-security": "max-age=31536000; includeSubDomains" }
src/release/webspec.ts +1
@@ -177,6 +177,7 @@ export function headerProblems(h: Headers, https: boolean): Problem[] {
177177 if ((h.get("x-content-type-options") ?? "").toLowerCase() !== "nosniff") add("nosniff", "X-Content-Type-Options is not nosniff", "nosniff");
178178 if (!h.get("referrer-policy")) add("referrer-policy", "no Referrer-Policy", "referrer-policy");
179179 if (!h.get("permissions-policy")) add("permissions-policy", "no Permissions-Policy", "permissions-policy");
180 if (!h.get("cross-origin-opener-policy")) add("coop", "no Cross-Origin-Opener-Policy", "coop", true);
180181 if (https && !h.get("strict-transport-security")) add("hsts", "no Strict-Transport-Security on an https URL", "hsts", true);
181182 if (h.has("x-xss-protection")) add("x-xss-protection", "X-XSS-Protection is set; drop it", "x-xss-protection");
182183 if (/\bimmutable\b/i.test(h.get("cache-control") ?? "")) add("cache-html", "HTML is cached immutable; only hashed assets may be", "caching");
test/scaffold-webspec.test.ts +7
@@ -17,6 +17,7 @@ const KNOWN: string[] = [];
1717 let dir: string;
1818 let child: ReturnType<typeof Bun.spawn> | undefined;
1919 let findings: Finding[] = [];
20 let served: Headers = new Headers();
2021
2122 beforeAll(async () => {
2223 dir = await mkdtemp(join(tmpdir(), "forge-scaffold-webspec-"));
@@ -48,6 +49,7 @@ beforeAll(async () => {
4849 if (Date.now() > end) throw new Error("the scaffold did not boot");
4950 await Bun.sleep(50);
5051 }
52 served = (await fetch(`${base}/`)).headers;
5153 findings = await webspec(base, ["/"], fetch, { site: true });
5254 }, 30_000);
5355
@@ -61,3 +63,8 @@ test("a fresh scaffold's webspec findings are exactly the known gaps", () => {
6163 const got = [...new Set(findings.map((f) => f.check))].sort();
6264 expect(got).toEqual([...KNOWN].sort());
6365 });
66
67 test("a fresh scaffold sends Cross-Origin-Opener-Policy: same-origin", () => {
68 expect(findings.map((f) => f.check)).not.toContain("coop");
69 expect(served.get("cross-origin-opener-policy")).toBe("same-origin");
70 });
test/webspec.test.ts +6
@@ -20,6 +20,7 @@ const GOOD_HEADERS: Record<string, string> = {
2020 "x-content-type-options": "nosniff",
2121 "referrer-policy": "same-origin",
2222 "permissions-policy": "camera=()",
23 "cross-origin-opener-policy": "same-origin",
2324 "strict-transport-security": "max-age=63072000",
2425 "cache-control": "no-cache",
2526 etag: '"v1"',
@@ -90,6 +91,11 @@ test("style-src unsafe-inline is not a script finding", async () => {
9091 expect(await checks(undefined, { ...GOOD_HEADERS, "content-security-policy": csp })).toEqual([]);
9192 });
9293
94 test("a missing Cross-Origin-Opener-Policy is an edge finding", async () => {
95 const found = await webspec("http://x.test", ["/"], site(GOOD_HTML, without("cross-origin-opener-policy")));
96 expect(found.map((f) => [f.check, f.edge])).toEqual([["coop", true]]);
97 });
98
9399 test("HSTS is an edge finding on https only", async () => {
94100 const found = await webspec("https://x.test", ["/"], site(GOOD_HTML, without("strict-transport-security")));
95101 expect(found.map((f) => [f.check, f.edge])).toEqual([["hsts", true]]);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs