From 3f35e4b18d581e05a12880a59bf020d836e2b6dd Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Sat, 7 Mar 2026 16:44:29 -0700 Subject: [PATCH] Harden MkDocs header auth-check: targeted postMessage, tighter CSP - Replace postMessage wildcard ('*') with explicit parent origin passed via ?origin= parameter to prevent auth state disclosure to arbitrary embedders - Tighten frame-ancestors CSP: production restricts to self + DOMAIN, dev restricts to localhost origins (was frame-ancestors *) - Remove deprecated X-Frame-Options ALLOW-FROM header (CSP frame-ancestors is the modern replacement) - Validate targetOrigin with URL constructor before use Bunker Admin --- admin/public/auth-check.html | 12 +++++++++++- api/src/modules/docs/header-builder.service.ts | 2 +- mkdocs/docs/overrides/main.html | 6 +----- nginx/conf.d/default.conf.template | 2 +- nginx/conf.d/services.conf.template | 1 - 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/admin/public/auth-check.html b/admin/public/auth-check.html index 917e4931..c5fc763a 100644 --- a/admin/public/auth-check.html +++ b/admin/public/auth-check.html @@ -5,6 +5,7 @@ // This page is loaded in a hidden iframe from the MkDocs header. // It reads the auth state from this origin's localStorage and // posts it back to the parent window via postMessage. +// The parent passes its origin as ?origin=... so we can target the reply. (function() { var authenticated = false; try { @@ -16,11 +17,20 @@ } } } catch(e) {} + // Only post back to the declared parent origin (prevents state disclosure to arbitrary embedders) + var params = new URLSearchParams(location.search); + var targetOrigin = params.get('origin'); + if (!targetOrigin) return; + // Validate targetOrigin is a proper origin (protocol + host, no path) + try { + var url = new URL(targetOrigin); + targetOrigin = url.origin; + } catch(e) { return; } if (window.parent && window.parent !== window) { window.parent.postMessage({ type: 'cml-auth-status', authenticated: authenticated - }, '*'); + }, targetOrigin); } })(); diff --git a/api/src/modules/docs/header-builder.service.ts b/api/src/modules/docs/header-builder.service.ts index c7ebc3c9..74198f60 100644 --- a/api/src/modules/docs/header-builder.service.ts +++ b/api/src/modules/docs/header-builder.service.ts @@ -716,7 +716,7 @@ class HeaderBuilderService { // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/mkdocs/docs/overrides/main.html b/mkdocs/docs/overrides/main.html index a1a01e25..14471db0 100644 --- a/mkdocs/docs/overrides/main.html +++ b/mkdocs/docs/overrides/main.html @@ -24,8 +24,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -89,8 +87,6 @@ scheduleShifts eventCalendar bar_chartPolls - sellTickets - videocamMeet play_circleGallery @@ -205,7 +201,7 @@ // 2. Cross-origin check via hidden iframe + postMessage var iframe = document.createElement('iframe'); iframe.style.display = 'none'; - iframe.src = base + '/auth-check.html'; + iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin); window.addEventListener('message', function(event) { if (event.origin !== base) return; if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) { diff --git a/nginx/conf.d/default.conf.template b/nginx/conf.d/default.conf.template index 09956015..0f7db57a 100644 --- a/nginx/conf.d/default.conf.template +++ b/nginx/conf.d/default.conf.template @@ -7,7 +7,7 @@ server { # Auth check iframe — allows cross-origin login state detection (MkDocs header) location = /auth-check.html { - add_header Content-Security-Policy "frame-ancestors *" always; + add_header Content-Security-Policy "frame-ancestors 'self' http://localhost:* http://127.0.0.1:*" always; set $upstream_admin_authcheck http://changemaker-v2-admin:3000; proxy_pass $upstream_admin_authcheck; proxy_set_header Host $host; diff --git a/nginx/conf.d/services.conf.template b/nginx/conf.d/services.conf.template index d369ae44..0d00d175 100644 --- a/nginx/conf.d/services.conf.template +++ b/nginx/conf.d/services.conf.template @@ -372,7 +372,6 @@ server { # Auth check iframe — allows root domain to embed this tiny page # for cross-origin login state detection (MkDocs header) location = /auth-check.html { - add_header X-Frame-Options "ALLOW-FROM https://${DOMAIN}" always; add_header Content-Security-Policy "frame-ancestors 'self' https://${DOMAIN} http://${DOMAIN}" always; set $upstream_admin_authcheck http://changemaker-v2-admin:3000; proxy_pass $upstream_admin_authcheck;