Skip to content

Commit 9e3503a

Browse files
viloforgeviloforge
authored andcommitted
feat(api): baseline security headers; suppress the MSAL local-network prompt
@azure/msal-browser v4's silent-auth iframe sets `allow="local-network-access *"` on itself so the embedded IdP page can probe a localhost SSO broker — which makes Chrome prompt the user "this page wants to access your other devices" on every fresh visit. Set `Permissions-Policy: local-network-access=(self)` on all responses: that caps the cross-origin delegation to the login.microsoftonline.com iframe, so the prompt never fires; the silent flow is unaffected (it doesn't need the broker). Also adds X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN (not DENY — the MSAL silent iframe briefly lands back on this origin), Referrer-Policy: strict-origin-when-cross-origin. A full CSP (DESIGN §D3) is left for a follow-up — it needs a vite.config change to drop the inline modulepreload polyfill first.
1 parent 3bfd26f commit 9e3503a

1 file changed

Lines changed: 24 additions & 1 deletion

File tree

packages/api/src/server/app.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import { existsSync } from "node:fs";
2121
import { dirname, resolve } from "node:path";
2222
import { fileURLToPath } from "node:url";
23-
import express, { type Express, type Request, type Response } from "express";
23+
import express, { type Express, type NextFunction, type Request, type Response } from "express";
2424
import { VERSION } from "../shared/version.js";
2525
import type { Verifier } from "./auth/idp.js";
2626
import type { Db } from "./db.js";
@@ -39,6 +39,29 @@ export function createApp(deps: AppDeps): Express {
3939
const app = express();
4040
app.disable("x-powered-by");
4141

42+
// Baseline security headers on every response.
43+
//
44+
// `Permissions-Policy: local-network-access=(self)` is load-bearing:
45+
// `@azure/msal-browser`'s silent-auth iframe (the hidden frame it uses for
46+
// SSO / token renewal) sets `allow="local-network-access *"` on itself so
47+
// the embedded IdP page can probe a localhost single-sign-on broker — which
48+
// makes Chrome prompt the user "this page wants to access your other
49+
// devices". Capping `local-network-access` to `(self)` here means that
50+
// cross-origin delegation (to the `login.microsoftonline.com` iframe) is
51+
// denied, so the prompt never fires; the silent flow works fine without the
52+
// broker. `X-Frame-Options` must stay SAMEORIGIN, not DENY — that same MSAL
53+
// silent iframe briefly lands back on *this* origin during the handshake.
54+
// (A full Content-Security-Policy is a worthwhile addition per DESIGN §D3
55+
// but needs a vite.config tweak to drop the inline modulepreload polyfill
56+
// first; out of scope here.)
57+
app.use((_req: Request, res: Response, next: NextFunction) => {
58+
res.setHeader("Permissions-Policy", "local-network-access=(self)");
59+
res.setHeader("X-Content-Type-Options", "nosniff");
60+
res.setHeader("X-Frame-Options", "SAMEORIGIN");
61+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
62+
next();
63+
});
64+
4265
app.get("/health", (_req: Request, res: Response) => {
4366
res.json({ status: "ok", version: VERSION });
4467
});

0 commit comments

Comments
 (0)