From f399367be39d5addf73c19ef782ba9360cbdb022 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Wed, 8 Sep 2021 16:37:40 +0200 Subject: [PATCH 1/4] Add nonce placeholders and option to toggle. --- packages/kit/src/core/build/index.js | 3 ++- packages/kit/src/core/config/options.js | 4 +++- packages/kit/src/core/dev/index.js | 3 ++- packages/kit/src/runtime/server/page/render.js | 17 ++++++++++------- packages/kit/types/config.d.ts | 1 + packages/kit/types/internal.d.ts | 1 + 6 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index 7ea97b239612..a9498a3a5057 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -349,7 +349,8 @@ async function build_server( ssr: ${s(config.kit.ssr)}, target: ${s(config.kit.target)}, template, - trailing_slash: ${s(config.kit.trailingSlash)} + trailing_slash: ${s(config.kit.trailingSlash)}, + noncePlaceholders: ${s(config.kit.noncePlaceholders)} }; } diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 213064e42e45..9f226ac36481 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -192,7 +192,9 @@ const options = object( return input; } - ) + ), + + noncePlaceholders: boolean(false) }) }, true diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 06253e6919e4..1d5ea86be19d 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -474,7 +474,8 @@ async function create_handler(vite, config, dir, cwd, get_manifest) { return rendered; }, - trailing_slash: config.kit.trailingSlash + trailing_slash: config.kit.trailingSlash, + noncePlaceholders: config.kit.noncePlaceholders } ); diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index dc3f9d247834..f147740744ef 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -29,6 +29,7 @@ export async function render_response({ const css = new Set(options.entry.css); const js = new Set(options.entry.js); const styles = new Set(); + const nonce = options.noncePlaceholders ? '%svelte.CSPNonce%' : ''; /** @type {Array<{ url: string, body: string, json: string }>} */ const serialized_data = []; @@ -96,10 +97,12 @@ export async function render_response({ // TODO strip the AMP stuff out of the build if not relevant const links = options.amp ? styles.size > 0 || rendered.css.code.length > 0 - ? `` + ? `` : '' : [ - ...Array.from(js).map((dep) => ``), + ...Array.from(js).map((dep) => ``), ...Array.from(css).map((dep) => ``) ].join('\n\t\t'); @@ -108,12 +111,12 @@ export async function render_response({ if (options.amp) { init = ` - + `; } else if (include_js) { // prettier-ignore - init = ``; + return ``; }) .join('\n\n\t')} `; diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 3ff95f46fbb2..5983866b8fbc 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -78,6 +78,7 @@ export interface Config { target?: string; trailingSlash?: TrailingSlash; vite?: ViteConfig | (() => ViteConfig); + noncePlaceholders?: boolean; }; preprocess?: any; } diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index fd7864c1acac..3879d6720f33 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -136,6 +136,7 @@ export interface SSRRenderOptions { target: string; template({ head, body }: { head: string; body: string }): string; trailing_slash: TrailingSlash; + noncePlaceholders: boolean; } export interface SSRRenderState { From ad8c8a8cabb215d961ba22e4aad1f5fe6ed85a1a Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Wed, 8 Sep 2021 16:46:11 +0200 Subject: [PATCH 2/4] Fix tests after adding onfig key --- packages/kit/src/core/config/index.spec.js | 6 ++++-- packages/kit/src/core/config/test/index.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index f110463fa9b2..49319887e0ba 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -56,7 +56,8 @@ test('fills in defaults', () => { router: true, ssr: true, target: null, - trailingSlash: 'never' + trailingSlash: 'never', + noncePlaceholders: false } }); }); @@ -164,7 +165,8 @@ test('fills in partial blanks', () => { router: true, ssr: true, target: null, - trailingSlash: 'never' + trailingSlash: 'never', + noncePlaceholders: false } }); }); diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index 3d7f7dfc9714..32ab3b198af2 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -63,7 +63,8 @@ async function testLoadDefaultConfig(path) { router: true, ssr: true, target: null, - trailingSlash: 'never' + trailingSlash: 'never', + noncePlaceholders: false } }); } From 3579e63106bdbdde1f424a6184680a54b9fb8700 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Wed, 8 Sep 2021 16:47:53 +0200 Subject: [PATCH 3/4] Changeset --- .changeset/lovely-rats-cheer.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lovely-rats-cheer.md diff --git a/.changeset/lovely-rats-cheer.md b/.changeset/lovely-rats-cheer.md new file mode 100644 index 000000000000..18ba7055d0c7 --- /dev/null +++ b/.changeset/lovely-rats-cheer.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Add option to place CSP nonce placeholders in rendered pages. From cc1618b0dc39eafb976ee0e8ca9c055c4af8ec2e Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Thu, 9 Sep 2021 17:18:56 +0200 Subject: [PATCH 4/4] Create 15-content-security-policy.md --- .../docs/15-content-security-policy.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 documentation/docs/15-content-security-policy.md diff --git a/documentation/docs/15-content-security-policy.md b/documentation/docs/15-content-security-policy.md new file mode 100644 index 000000000000..a404e7d1d3fe --- /dev/null +++ b/documentation/docs/15-content-security-policy.md @@ -0,0 +1,49 @@ +--- +title: Content Security Policy +--- + +At the moment, SvelteKit supports adding Content Security Policy via hooks. In environments with a runtime, HTTP headers can be added to the response object. + +However, SvelteKit also requires some small pieces of inline JavaScript in order for hydration to work. To avoid using `'unsafe-inline'` (which, as the name suggests, should be avoided), SvelteKit can be configured to inject place-holders for CSP Nonces into the html it generates. + +These placeholders take the form `%svelte.CSPNonce%`, and can be replaced with a nonce inside the hook. A basic CSP handler hook might then look like this: +```javascript +export async function handle ({ request, resolve }) => { + const directives = { + 'default-src': ["'self'", rootDomain, `ws://${rootDomain}`], + 'script-src': ["'strict-dynamic'"], + 'style-src': ["'self'"] + }; + + const response = await resolve(request); + + const nonce = randomBytes(32).toString('base64'); + + if (response.headers['content-type'] === 'text/html') { + response.body = (response.body as string).replace(/%svelte.CSPNonce%/g, `nonce="${nonce}"`); + } + directives['script-src'].push(`'nonce-${nonce}'`); + directives['style-src'].push(`'nonce-${nonce}'`); + + if (process.env.NODE_ENV === 'development') { + directives['style-src'].push('unsafe-inline') + } + + const csp = Object.entries(directives) + .map(([key, arr]) => key + ' ' + arr.join(' ')) + .join('; '); + + return { + ...response, + headers: { + ...response.headers, + 'Content-Security-Policy': csp + } + }; +}; +``` +Because of the way Vite performs hot reloads of stylesheets, `'unsafe-inline'` is required in dev mode. + +Be warned: some other features of Svelte (in particular CSS transitions and animations) might run afoul of this Content Security Policy and require either rewriting to JS-based transitions or enabling `style-src: 'unsafe-inline'`. + +The nonce placeholders can be toggled with the `kit.noncePlaceholders` configuration option.