From 63e5a89f979e2bf7c526a88193e762ab5e4853d4 Mon Sep 17 00:00:00 2001 From: mm-kevcenteno Date: Tue, 28 Jul 2026 10:15:27 -0400 Subject: [PATCH 1/4] Remove deprecated headers and stale policy features Feature-Policy was superseded by Permissions-Policy. X-XSS-Protection is non-standard, deprecated, and can itself introduce XSS vulnerabilities in otherwise safe pages; our Content-Security-Policy covers what it was for. Also drop the Permissions-Policy features that no browser recognizes, verified against Chrome 149 and 150: ambient-light-sensor, battery, document-domain, execution-while-not-rendered, execution-while-out-of-viewport, and speaker-selection. Chrome reported each of these as an unrecognized feature, so they never had any effect. --- bin/_headers.config.ts | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/bin/_headers.config.ts b/bin/_headers.config.ts index a08cf8fb8..b383d2c80 100644 --- a/bin/_headers.config.ts +++ b/bin/_headers.config.ts @@ -120,33 +120,17 @@ const config: HeadersConfig = { 'https://www.gstatic.com', ], }, - 'Feature-Policy': [ - "accelerometer 'none'", - "autoplay 'none'", - "camera 'none'", - "encrypted-media 'none'", - "fullscreen 'none'", - "geolocation 'none'", - "gyroscope 'none'", - "magnetometer 'none'", - "microphone 'none'", - "midi 'none'", - "payment 'none'", - "picture-in-picture 'none'", - "usb 'none'", - "sync-xhr 'none'", - ], + // While many of these features do not seem to have any security + // implication, deny them out of an abundance of caution to limit what + // injected or embedded content can reach. See + // https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md + // for the features that exist. 'Permissions-Policy': [ 'accelerometer=()', - 'ambient-light-sensor=()', 'autoplay=()', - 'battery=()', 'camera=()', 'display-capture=()', - 'document-domain=()', 'encrypted-media=()', - 'execution-while-not-rendered=()', - 'execution-while-out-of-viewport=()', 'fullscreen=()', 'gamepad=()', 'geolocation=()', @@ -161,7 +145,6 @@ const config: HeadersConfig = { 'publickey-credentials-get=()', 'screen-wake-lock=()', 'serial=()', - 'speaker-selection=()', 'usb=()', 'web-share=()', 'xr-spatial-tracking=()', @@ -174,7 +157,6 @@ const config: HeadersConfig = { ], 'X-Content-Type-Options': ['nosniff'], 'X-Frame-Options': ['DENY'], - 'X-XSS-Protection': ['1', 'mode=block'], }, }, ], From db9968c7875249bc4cde7fffcb2d7ae703c80004 Mon Sep 17 00:00:00 2001 From: mm-kevcenteno Date: Fri, 31 Jul 2026 11:12:42 -0400 Subject: [PATCH 2/4] Keep denying sync-xhr after the Feature-Policy removal sync-xhr was denied only via Feature-Policy and had no Permissions-Policy counterpart, so removing that header dropped the denial entirely. Chrome still enforces Feature-Policy, and sync-xhr defaults to an allowlist of *, so this was blocking synchronous XHR in the top document and in every cross-origin frame. --- bin/_headers.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/_headers.config.ts b/bin/_headers.config.ts index b383d2c80..799777883 100644 --- a/bin/_headers.config.ts +++ b/bin/_headers.config.ts @@ -145,6 +145,7 @@ const config: HeadersConfig = { 'publickey-credentials-get=()', 'screen-wake-lock=()', 'serial=()', + 'sync-xhr=()', 'usb=()', 'web-share=()', 'xr-spatial-tracking=()', From 344762b4201e2d03163343c9ffe5e7c8296c6a76 Mon Sep 17 00:00:00 2001 From: mm-kevcenteno Date: Wed, 26 Aug 2026 15:49:04 -0400 Subject: [PATCH 3/4] Deny 53 more Permissions-Policy features This brings the policy to the same 76 features mm_website denies. The list was 22 features short of mm_website, and a further 31 recognized features were denied on none of the three sites. clipboard-write allows self because the copy button in assets/js/copy-markdown.ts calls navigator.clipboard.writeText. Every other addition is a flat deny. static/_headers is generated, so run npm run build:headers to pick this up. --- bin/_headers.config.ts | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/bin/_headers.config.ts b/bin/_headers.config.ts index 799777883..e290d4670 100644 --- a/bin/_headers.config.ts +++ b/bin/_headers.config.ts @@ -127,8 +127,41 @@ const config: HeadersConfig = { // for the features that exist. 'Permissions-Policy': [ 'accelerometer=()', + 'aria-notify=()', 'autoplay=()', + 'bluetooth=()', + 'browsing-topics=()', 'camera=()', + 'captured-surface-control=()', + 'ch-device-memory=()', + 'ch-downlink=()', + 'ch-dpr=()', + 'ch-ect=()', + 'ch-prefers-color-scheme=()', + 'ch-prefers-reduced-motion=()', + 'ch-prefers-reduced-transparency=()', + 'ch-rtt=()', + 'ch-save-data=()', + 'ch-ua=()', + 'ch-ua-arch=()', + 'ch-ua-bitness=()', + 'ch-ua-form-factors=()', + 'ch-ua-full-version=()', + 'ch-ua-full-version-list=()', + 'ch-ua-high-entropy-values=()', + 'ch-ua-mobile=()', + 'ch-ua-model=()', + 'ch-ua-platform=()', + 'ch-ua-platform-version=()', + 'ch-ua-wow64=()', + 'ch-viewport-height=()', + 'ch-viewport-width=()', + 'ch-width=()', + 'clipboard-read=()', + 'clipboard-write=(self)', + 'compute-pressure=()', + 'cross-origin-isolated=()', + 'digital-credentials-get=()', 'display-capture=()', 'encrypted-media=()', 'fullscreen=()', @@ -136,18 +169,38 @@ const config: HeadersConfig = { 'geolocation=()', 'gyroscope=()', 'hid=()', + 'identity-credentials-get=()', 'idle-detection=()', + 'interest-cohort=()', + 'join-ad-interest-group=()', + 'keyboard-map=()', + 'language-detector=()', + 'language-model=()', + 'local-fonts=()', + 'local-network=()', + 'local-network-access=()', + 'loopback-network=()', 'magnetometer=()', 'microphone=()', 'midi=()', + 'on-device-speech-recognition=()', + 'otp-credentials=()', 'payment=()', 'picture-in-picture=()', + 'private-state-token-issuance=()', + 'private-state-token-redemption=()', + 'publickey-credentials-create=()', 'publickey-credentials-get=()', + 'run-ad-auction=()', 'screen-wake-lock=()', 'serial=()', + 'summarizer=()', 'sync-xhr=()', + 'translator=()', + 'unload=()', 'usb=()', 'web-share=()', + 'window-management=()', 'xr-spatial-tracking=()', ], 'Referrer-Policy': ['strict-origin-when-cross-origin'], From 2516be78a52dc68706189d0823d1e4aea9dd9f59 Mon Sep 17 00:00:00 2001 From: kevcenteno Date: Wed, 2 Sep 2026 17:42:40 -0400 Subject: [PATCH 4/4] Drop the two ad-auction features Chromium is removing Chromium is removing join-ad-interest-group and run-ad-auction, so the denials become dead weight once the names go. This is not free today. Chrome 152 still recognizes both and raises no warning for them, so a page can call the Protected Audience API where the denial used to stop it. browsing-topics stays denied, which is the publisher-side feature of that family with a name that is not going away. Record why the policy allows what it allows, on both sides: the features listed with an allowlist, and the features left off the list entirely. Neither should read as an oversight. static/_headers is generated, so run npm run build:headers to pick this up. --- bin/_headers.config.ts | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/bin/_headers.config.ts b/bin/_headers.config.ts index e290d4670..a007c0f4e 100644 --- a/bin/_headers.config.ts +++ b/bin/_headers.config.ts @@ -125,6 +125,27 @@ const config: HeadersConfig = { // injected or embedded content can reach. See // https://github.com/w3c/webappsec-permissions-policy/blob/main/features.md // for the features that exist. + // + // Allowed for our own origin, because our own code calls the API: + // - clipboard-write: the copy button in + // assets/js/copy-markdown.ts calls + // navigator.clipboard.writeText. + // + // That feature defaults to an allowlist of *, so (self) is not the + // same as dropping the entry. A cross-origin embed stays denied. + // + // Absent from the list, so not denied: + // - attribution-reporting: Google Ads conversion measurement uses + // it, and we are the advertiser. The publisher-side features of + // the same family cost us nothing, which is why browsing-topics is + // denied below. + // - join-ad-interest-group, run-ad-auction: publisher-side too, so + // denying them would also be free, but Chromium is removing the + // names. + // + // shared-storage, shared-storage-select-url and private-aggregation + // are publisher-side and might be safe to deny. The ads and analytics + // tags come from Google Tag Manager, so what they use is a black box. 'Permissions-Policy': [ 'accelerometer=()', 'aria-notify=()', @@ -172,7 +193,6 @@ const config: HeadersConfig = { 'identity-credentials-get=()', 'idle-detection=()', 'interest-cohort=()', - 'join-ad-interest-group=()', 'keyboard-map=()', 'language-detector=()', 'language-model=()', @@ -191,7 +211,6 @@ const config: HeadersConfig = { 'private-state-token-redemption=()', 'publickey-credentials-create=()', 'publickey-credentials-get=()', - 'run-ad-auction=()', 'screen-wake-lock=()', 'serial=()', 'summarizer=()',