Files
webmail/lib/security/app-frame-origins-client.ts
T
Linus Rath 3ac6c25c71 fix: keep sibling subdomains from widening the sidebar-app frame-src
The inline sidebar apps hand their origins to the proxy through a
client-written cookie, which the proxy merges into `frame-src`. Any
same-site sibling host could set that cookie too. Over HTTPS the cookie
is now `__Host-bulwark_app_frame_origins`, which only this exact host
can set, and the proxy ignores the unprefixed name there. Plain HTTP
(local development) keeps the old name.
2026-09-26 16:27:03 +02:00

58 lines
1.9 KiB
TypeScript

/**
* Browser side of the Sidebar Apps CSP handshake. See
* `lib/security/app-frame-origins.ts` for why the cookie exists.
*/
import { getPathPrefix } from '@/lib/browser-navigation';
import {
APP_FRAME_ORIGINS_COOKIE,
APP_FRAME_ORIGINS_SECURE_COOKIE,
parseAppFrameOrigins,
serializeAppFrameOrigins,
} from './app-frame-origins';
/** `__Host-` over HTTPS, see APP_FRAME_ORIGINS_SECURE_COOKIE. */
function cookieName(): string {
return typeof window !== 'undefined' && window.location.protocol === 'https:'
? APP_FRAME_ORIGINS_SECURE_COOKIE
: APP_FRAME_ORIGINS_COOKIE;
}
function readRawCookie(name: string): string | null {
if (typeof document === 'undefined') return null;
for (const part of document.cookie.split(';')) {
const eq = part.indexOf('=');
if (eq === -1) continue;
if (part.slice(0, eq).trim() !== name) continue;
return part.slice(eq + 1).trim();
}
return null;
}
/** Origins currently advertised to the proxy, validated. */
export function readAppFrameOrigins(): string[] {
return parseAppFrameOrigins(readRawCookie(cookieName()));
}
/**
* Publishes the origins for the proxy to merge into `frame-src`. Returns
* whether the write actually landed - if cookies are blocked it returns false
* and callers must not wait for a CSP that will never widen.
*/
export function writeAppFrameOrigins(origins: ReadonlyArray<string>): boolean {
if (typeof document === 'undefined') return false;
const value = serializeAppFrameOrigins(origins);
const name = cookieName();
const secure = name === APP_FRAME_ORIGINS_SECURE_COOKIE;
const attrs = [
`${name}=${value}`,
// A __Host- cookie must be on Path=/.
`path=${secure ? '/' : getPathPrefix() || '/'}`,
'max-age=31536000',
'samesite=lax',
];
if (secure) attrs.push('secure');
document.cookie = attrs.join('; ');
return readRawCookie(name) === value;
}