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
This commit is contained in:
bunker-admin 2026-03-07 16:44:29 -07:00
parent eba6453981
commit 3f35e4b18d
5 changed files with 14 additions and 9 deletions

View File

@ -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);
}
})();
</script>

View File

@ -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) {

View File

@ -24,8 +24,6 @@
<a href="#" data-path="/shifts" class="cm-header-nav__dropdown-item" data-nav-id="shifts"><span class="material-icons-outlined">schedule</span><span>Shifts</span></a>
<a href="#" data-path="/events" class="cm-header-nav__dropdown-item" data-nav-id="events"><span class="material-icons-outlined">event</span><span>Calendar</span></a>
<a href="#" data-path="/polls" class="cm-header-nav__dropdown-item" data-nav-id="polls"><span class="material-icons-outlined">bar_chart</span><span>Polls</span></a>
<a href="#" data-path="/events/tickets" class="cm-header-nav__dropdown-item" data-nav-id="tickets"><span class="material-icons-outlined">sell</span><span>Tickets</span></a>
<a href="#" data-path="/meet" class="cm-header-nav__dropdown-item" data-nav-id="meet"><span class="material-icons-outlined">videocam</span><span>Meet</span></a>
</div>
</div>
<a href="#" data-path="/gallery" class="cm-header-nav__link" data-nav-id="gallery"><span class="material-icons-outlined">play_circle</span><span class="cm-header-nav__label">Gallery</span></a>
@ -89,8 +87,6 @@
<a href="#" data-path="/shifts" class="cm-header-nav__mobile-link" data-nav-id="shifts" style="padding-left:48px"><span class="material-icons-outlined">schedule</span><span>Shifts</span></a>
<a href="#" data-path="/events" class="cm-header-nav__mobile-link" data-nav-id="events" style="padding-left:48px"><span class="material-icons-outlined">event</span><span>Calendar</span></a>
<a href="#" data-path="/polls" class="cm-header-nav__mobile-link" data-nav-id="polls" style="padding-left:48px"><span class="material-icons-outlined">bar_chart</span><span>Polls</span></a>
<a href="#" data-path="/events/tickets" class="cm-header-nav__mobile-link" data-nav-id="tickets" style="padding-left:48px"><span class="material-icons-outlined">sell</span><span>Tickets</span></a>
<a href="#" data-path="/meet" class="cm-header-nav__mobile-link" data-nav-id="meet" style="padding-left:48px"><span class="material-icons-outlined">videocam</span><span>Meet</span></a>
</div>
</div>
<a href="#" data-path="/gallery" class="cm-header-nav__mobile-link" data-nav-id="gallery"><span class="material-icons-outlined">play_circle</span><span>Gallery</span></a>
@ -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) {

View File

@ -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;

View File

@ -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;