diff --git a/.changeset/whole-needles-remain.md b/.changeset/whole-needles-remain.md new file mode 100644 index 0000000..a845151 --- /dev/null +++ b/.changeset/whole-needles-remain.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/app.ts b/server/app.ts index 66c89b1..71e9415 100644 --- a/server/app.ts +++ b/server/app.ts @@ -287,6 +287,13 @@ export function createApp({ maxHoldConnections = DEFAULT_MAX_HOLD_CONNECTIONS, }: AppOptions) { const app = new Hono(); + // `?key=` bootstraps cookie auth, so never let a board URL disclose that + // credential to another origin through an outbound Referer header. Set this + // before auth so denied and public routes carry the same policy. + app.use("*", (c, next) => { + c.header("Referrer-Policy", "no-referrer"); + return next(); + }); const bus = new EventBus(); if (onEvent) { bus.subscribe((event) => { diff --git a/test/api.test.ts b/test/api.test.ts index a843aaa..db1ac20 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -1171,6 +1171,20 @@ test("auth token guards mutating routes when configured", async () => { assert.equal(viaCookie.status, 200); }); +test("Referrer-Policy protects public, denied, and authenticated board responses", async () => { + const app = makeApp("secret"); + const responses = await Promise.all([ + app.request("/guide"), + app.request("/"), + app.request("/?key=secret"), + app.request("/api/sessions"), + ]); + + for (const response of responses) { + assert.equal(response.headers.get("referrer-policy"), "no-referrer"); + } +}); + async function readSseUntil(res: Response, needle: string, abort?: () => void): Promise { assert.ok(res.body); const reader = res.body.getReader(); diff --git a/test/workerIntegration.integration.ts b/test/workerIntegration.integration.ts index 3b16e6a..43d7c89 100644 --- a/test/workerIntegration.integration.ts +++ b/test/workerIntegration.integration.ts @@ -169,6 +169,7 @@ test( assert.equal(rendered.status, 200); assert.match(await rendered.text(), /worker-marker/); assert.equal(rendered.headers.get("content-security-policy"), "sandbox allow-scripts"); + assert.equal(rendered.headers.get("referrer-policy"), "no-referrer"); assert.equal(rendered.headers.get("x-content-type-options"), "nosniff"); assert.match(rendered.headers.get("cache-control") ?? "", /immutable/);