Public: anyone can read this; the crew can change it.
Every forge response carries Cross-Origin-Opener-Policy: same-origin #3402
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3398-every-forge-response-carries-cross-origi → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +12
−1
src/http/respond.ts +5 −1
| @@ -138,7 +138,9 @@ const HSTS = "max-age=31536000"; | ||
| 138 | 138 | /** |
| 139 | 139 | * The security floor every response the forge writes leaves with: never |
| 140 | 140 | * sniffed into another type, never a referrer sent off-site, no powers a |
| 141 | * page does not use, https once https, and — unless the body is a document | |
| 141 | * page does not use, https once https, no window another site opened or | |
| 142 | * opens kept in reach (Cross-Origin-Opener-Policy; the sign-in and consent | |
| 143 | * flows redirect and never read `window.opener`), and — unless the body is a document | |
| 142 | 144 | * that wrote its own policy — inert. |
| 143 | 145 | * |
| 144 | 146 | * A responder that already set one of these keeps its value: `page()` has a |
| @@ -158,6 +160,8 @@ export function securityFloor(response: Response): Response { | ||
| 158 | 160 | headers.set("permissions-policy", PERMISSIONS_POLICY); |
| 159 | 161 | if (!headers.has("strict-transport-security")) |
| 160 | 162 | headers.set("strict-transport-security", HSTS); |
| 163 | if (!headers.has("cross-origin-opener-policy")) | |
| 164 | headers.set("cross-origin-opener-policy", "same-origin"); | |
| 161 | 165 | // What left decides, not what was asked for: an HTML body writes the |
| 162 | 166 | // only policy that can name its own nonce, and `default-src 'none'` |
| 163 | 167 | // over a page would strip it of its stylesheet. |
test/web.test.ts +7
| @@ -814,6 +814,9 @@ function expectFloor(what: string, response: Response): void { | ||
| 814 | 814 | expect(label + response.headers.get("strict-transport-security")).toBe( |
| 815 | 815 | label + "max-age=31536000", |
| 816 | 816 | ); |
| 817 | expect(label + response.headers.get("cross-origin-opener-policy")).toBe( | |
| 818 | label + "same-origin", | |
| 819 | ); | |
| 817 | 820 | const policy = response.headers.get("content-security-policy") ?? ""; |
| 818 | 821 | expect(label + (policy === "" ? "(none)" : "(a policy)")).toBe( |
| 819 | 822 | label + "(a policy)", |
| @@ -898,9 +901,13 @@ describe("the security floor is on every response", () => { | ||
| 898 | 901 | "content-type": "text/html; charset=utf-8", |
| 899 | 902 | "content-security-policy": "default-src 'none'; sandbox", |
| 900 | 903 | "referrer-policy": "no-referrer", |
| 904 | "cross-origin-opener-policy": "same-origin-allow-popups", | |
| 901 | 905 | }, |
| 902 | 906 | }), |
| 903 | 907 | ); |
| 908 | expect(own.headers.get("cross-origin-opener-policy")).toBe( | |
| 909 | "same-origin-allow-popups", | |
| 910 | ); | |
| 904 | 911 | expect(own.headers.get("content-security-policy")).toBe( |
| 905 | 912 | "default-src 'none'; sandbox", |
| 906 | 913 | ); |
new above
offline
Right default.
same-origin, notsame-origin-allow-popups, is correct because no forge page opens a popup that needs a handle back.