mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
ae5cd6a159
* 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.
115 lines
3.3 KiB
TypeScript
115 lines
3.3 KiB
TypeScript
import crypto from "node:crypto";
|
|
import type { Context, Next } from "koa";
|
|
import { contentSecurityPolicy } from "koa-helmet";
|
|
import { uniq } from "es-toolkit/compat";
|
|
import env from "@server/env";
|
|
|
|
const getBucketOrigin = () => {
|
|
if (env.AWS_S3_ACCELERATE_URL) {
|
|
return new URL(env.AWS_S3_ACCELERATE_URL).origin;
|
|
}
|
|
|
|
const url = env.AWS_S3_UPLOAD_BUCKET_URL || "";
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const parsedUrl = new URL(url);
|
|
|
|
if (
|
|
env.AWS_S3_UPLOAD_BUCKET_NAME &&
|
|
parsedUrl.hostname.startsWith(`${env.AWS_S3_UPLOAD_BUCKET_NAME}.`)
|
|
) {
|
|
const hostnameWithoutBucket = parsedUrl.hostname.substring(
|
|
env.AWS_S3_UPLOAD_BUCKET_NAME.length + 1 // +1 for the dot
|
|
);
|
|
return `${parsedUrl.protocol}//${hostnameWithoutBucket}`;
|
|
}
|
|
|
|
return parsedUrl.origin;
|
|
} catch {
|
|
return;
|
|
}
|
|
};
|
|
|
|
interface CSPOptions {
|
|
/** Additional origins to allow as script sources. */
|
|
extraScriptSrc?: string[];
|
|
}
|
|
|
|
/**
|
|
* Create a Content Security Policy middleware for the application.
|
|
*
|
|
* @param options Optional configuration for the CSP middleware.
|
|
* @returns A Koa middleware function that applies the CSP headers.
|
|
*/
|
|
export default function createCSPMiddleware(options?: CSPOptions) {
|
|
// Construct scripts CSP based on options in use
|
|
const defaultSrc: string[] = ["'self'"];
|
|
const scriptSrc: string[] = [];
|
|
const styleSrc: string[] = ["'self'", "'unsafe-inline'"];
|
|
const objectSrc: string[] = [env.URL, "'self'"];
|
|
|
|
if (env.isCloudHosted) {
|
|
scriptSrc.push("www.googletagmanager.com");
|
|
scriptSrc.push("cdn.zapier.com");
|
|
styleSrc.push("cdn.zapier.com");
|
|
}
|
|
|
|
// Allow to load assets from Vite
|
|
if (!env.isProduction) {
|
|
scriptSrc.push(env.URL.replace(`:${env.PORT}`, ":3001"));
|
|
scriptSrc.push("localhost:3001");
|
|
} else {
|
|
scriptSrc.push(env.URL);
|
|
}
|
|
|
|
if (env.GOOGLE_ANALYTICS_ID) {
|
|
scriptSrc.push("www.googletagmanager.com");
|
|
scriptSrc.push("www.google-analytics.com");
|
|
}
|
|
|
|
if (env.CDN_URL) {
|
|
scriptSrc.push(env.CDN_URL);
|
|
styleSrc.push(env.CDN_URL);
|
|
defaultSrc.push(env.CDN_URL);
|
|
}
|
|
|
|
const bucketOrigin = getBucketOrigin();
|
|
if (bucketOrigin) {
|
|
objectSrc.push(bucketOrigin);
|
|
}
|
|
|
|
return function cspMiddleware(ctx: Context, next: Next) {
|
|
ctx.state.cspNonce = crypto.randomBytes(16).toString("hex");
|
|
|
|
// 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);
|
|
};
|
|
}
|