fix: Allow service worker to load on custom domains (#12502)

* fix: Allow service worker to load on custom domains

Add explicit worker-src 'self' so the service worker can register on
team custom domains. Without it, browsers fall back to script-src which
only lists env.URL and env.CDN_URL, blocking /static/sw.js on hosts
like docs.getoutline.com.

* fix: Switch worker-src approach to script-src 'self' for type safety

The @types/koa-helmet definitions don't include workerSrc. Add 'self'
to script-src instead — worker-src falls back to script-src per spec,
and 'self' matches the document origin on custom domains.

* fix: Properly add worker-src directive without script-src widening

Extract the CSP directives to a local variable so workerSrc can be
included despite koa-helmet's outdated type definitions missing it
(the underlying helmet supports it). Also drop @types/koa-helmet
since the package now ships its own (equivalent) types.
This commit is contained in:
Tom Moor
2026-05-28 09:07:05 -04:00
committed by GitHub
parent d2a0bf9923
commit ae5cd6a159
3 changed files with 27 additions and 35 deletions
+26 -21
View File
@@ -84,26 +84,31 @@ export default function createCSPMiddleware(options?: CSPOptions) {
return function cspMiddleware(ctx: Context, next: Next) {
ctx.state.cspNonce = crypto.randomBytes(16).toString("hex");
return contentSecurityPolicy({
directives: {
baseUri: ["'none'"],
defaultSrc,
styleSrc,
scriptSrc: [
...uniq(scriptSrc),
...(options?.extraScriptSrc ?? []),
env.DEVELOPMENT_UNSAFE_INLINE_CSP
? "'unsafe-inline'"
: `'nonce-${ctx.state.cspNonce}'`,
],
mediaSrc: ["*", "data:", "blob:"],
imgSrc: ["*", "data:", "blob:"],
frameSrc: ["*", "data:"],
objectSrc,
// Do not use connect-src: because self + websockets does not work in
// Safari, ref: https://bugs.webkit.org/show_bug.cgi?id=201591
connectSrc: ["*"],
},
})(ctx, next);
// Note: workerSrc is included even though it's missing from the koa-helmet
// type definitions — the underlying helmet supports it. The service worker
// is served from the same origin as the document, which may be a custom
// domain that is not present in scriptSrc.
const directives = {
baseUri: ["'none'"],
defaultSrc,
styleSrc,
scriptSrc: [
...uniq(scriptSrc),
...(options?.extraScriptSrc ?? []),
env.DEVELOPMENT_UNSAFE_INLINE_CSP
? "'unsafe-inline'"
: `'nonce-${ctx.state.cspNonce}'`,
],
mediaSrc: ["*", "data:", "blob:"],
imgSrc: ["*", "data:", "blob:"],
frameSrc: ["*", "data:"],
workerSrc: ["'self'"],
objectSrc,
// Do not use connect-src: because self + websockets does not work in
// Safari, ref: https://bugs.webkit.org/show_bug.cgi?id=201591
connectSrc: ["*"],
};
return contentSecurityPolicy({ directives })(ctx, next);
};
}