From bc576371c8c41c8c305beb23bb7f1af3d06c3843 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Thu, 9 Sep 2021 20:41:29 +0200 Subject: [PATCH 01/41] Add nonce generation to request handling. --- packages/kit/src/core/build/index.js | 6 +++-- packages/kit/src/core/config/index.spec.js | 6 +++-- packages/kit/src/core/config/options.js | 2 ++ packages/kit/src/core/config/test/index.js | 3 ++- packages/kit/src/core/dev/index.js | 3 ++- packages/kit/src/runtime/server/index.js | 9 ++++++-- .../kit/src/runtime/server/page/render.js | 23 +++++++++++-------- .../kit/src/runtime/server/page/respond.js | 3 ++- packages/kit/types/config.d.ts | 1 + packages/kit/types/internal.d.ts | 1 + 10 files changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index 666a2c42362c..4c621ac07529 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -343,7 +343,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)}, + cspNonce: ${s(config.kit.cspNonce)} }; } @@ -470,7 +471,8 @@ async function build_server( chunkFileNames: 'chunks/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash][extname]' }, - preserveEntrySignatures: 'strict' + preserveEntrySignatures: 'strict', + external: ['node:crypto'] } }, plugins: [ diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 2707b6133c2d..517c930bd42f 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -52,7 +52,8 @@ test('fills in defaults', () => { router: true, ssr: true, target: null, - trailingSlash: 'never' + trailingSlash: 'never', + cspNonce: false } }); }); @@ -152,7 +153,8 @@ test('fills in partial blanks', () => { router: true, ssr: true, target: null, - trailingSlash: 'never' + trailingSlash: 'never', + cspNonce: false } }); }); diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index 1a76bb14e113..1f70cb53809d 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -171,6 +171,8 @@ const options = object( trailingSlash: list(['never', 'always', 'ignore']), + cspNonce: boolean(false), + vite: validate( () => ({}), (input, keypath) => { diff --git a/packages/kit/src/core/config/test/index.js b/packages/kit/src/core/config/test/index.js index 57d966c14909..85f3b727719b 100644 --- a/packages/kit/src/core/config/test/index.js +++ b/packages/kit/src/core/config/test/index.js @@ -54,7 +54,8 @@ async function testLoadDefaultConfig(path) { router: true, ssr: true, target: null, - trailingSlash: 'never' + trailingSlash: 'never', + cspNonce: false } }); } diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 11e247c3b1fb..67f7cbef0cbc 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -510,7 +510,8 @@ async function create_plugin(config, dir, cwd, get_manifest) { return rendered; }, - trailing_slash: config.kit.trailingSlash + trailing_slash: config.kit.trailingSlash, + cspNonce: config.kit.cspNonce } ); diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 52d4f05d5ee3..5b67573fee04 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -7,6 +7,7 @@ import { lowercase_keys } from './utils.js'; import { hash } from '../hash.js'; import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; +import { randomBytes } from 'node:crypto'; /** @type {import('@sveltejs/kit/ssr').Respond} */ export async function respond(incoming, options, state = {}) { @@ -32,12 +33,15 @@ export async function respond(incoming, options, state = {}) { } const headers = lowercase_keys(incoming.headers); + const nonce = randomBytes(32).toString('base64'); const request = { ...incoming, headers, body: parse_body(incoming.rawBody, headers), params: {}, - locals: {} + locals: { + nonce + } }; try { @@ -50,7 +54,8 @@ export async function respond(incoming, options, state = {}) { $session: await options.hooks.getSession(request), page_config: { ssr: false, router: true, hydrate: true }, status: 200, - branch: [] + branch: [], + nonce }); } diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index bbd04e773b6e..689aac4df80f 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -16,7 +16,8 @@ const s = JSON.stringify; * page_config: { hydrate: boolean, router: boolean, ssr: boolean }; * status: number; * error?: Error, - * page?: import('types/page').Page + * page?: import('types/page').Page, + * nonce?: string * }} opts */ export async function render_response({ @@ -26,11 +27,13 @@ export async function render_response({ page_config, status, error, - page + page, + nonce }) { const css = new Set(options.entry.css); const js = new Set(options.entry.js); const styles = new Set(); + nonce = options.cspNonce ? `nonce="${nonce}"` : ''; /** @type {Array<{ url: string, body: string, json: string }>} */ const serialized_data = []; @@ -98,10 +101,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'); @@ -110,7 +115,7 @@ export async function render_response({ if (options.amp) { init = ` - + `; init += options.service_worker @@ -118,7 +123,7 @@ export async function render_response({ : ''; } else if (include_js) { // prettier-ignore - init = ``; + return ``; }) .join('\n\n\t')} `; diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js index 1e79cd504bc0..5ca0805349fb 100644 --- a/packages/kit/src/runtime/server/page/respond.js +++ b/packages/kit/src/runtime/server/page/respond.js @@ -200,7 +200,8 @@ export async function respond(opts) { page_config, status, error, - branch: branch.filter(Boolean) + branch: branch.filter(Boolean), + nonce: request.locals.nonce }), set_cookie_headers ); diff --git a/packages/kit/types/config.d.ts b/packages/kit/types/config.d.ts index 5d2214d0eacb..2be1f037ac5d 100644 --- a/packages/kit/types/config.d.ts +++ b/packages/kit/types/config.d.ts @@ -88,6 +88,7 @@ export interface Config { ssr?: boolean; target?: string; trailingSlash?: TrailingSlash; + cspNonce?: boolean; vite?: ViteConfig | (() => ViteConfig); }; preprocess?: any; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index bfc5b5ab0a05..87dff3663aad 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -159,6 +159,7 @@ export interface SSRRenderOptions { target: string; template({ head, body }: { head: string; body: string }): string; trailing_slash: TrailingSlash; + cspNonce: boolean; } export interface SSRRenderState { From c1f00ece94fe777387d4f50917f67315d1cfbcce Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Thu, 9 Sep 2021 20:43:48 +0200 Subject: [PATCH 02/41] Changeset --- .changeset/angry-schools-wait.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/angry-schools-wait.md diff --git a/.changeset/angry-schools-wait.md b/.changeset/angry-schools-wait.md new file mode 100644 index 000000000000..f5c69ddbd174 --- /dev/null +++ b/.changeset/angry-schools-wait.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Add cspNonce config to generate CSP nonces for all scripts and stylesheets. From 03cc65367cee3e576060ac7e5059dd3b6fcdfae9 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Thu, 9 Sep 2021 20:56:16 +0200 Subject: [PATCH 03/41] [docs] document CSP Nonces. --- .../docs/14-content-security-policy.md | 49 +++++++++++++++++++ ...4-configuration.md => 15-configuration.md} | 5 ++ 2 files changed, 54 insertions(+) create mode 100644 documentation/docs/14-content-security-policy.md rename documentation/docs/{14-configuration.md => 15-configuration.md} (98%) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md new file mode 100644 index 000000000000..f8734a3e0722 --- /dev/null +++ b/documentation/docs/14-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 CSP Nonces into the html it generates. + +The nonce value is availiable to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: + +```javascript +export async function handle ({ request, resolve }) => { + const directives = { + 'default-src': ["'self'", 'static.someotherdomain.com'], + 'script-src': ["'strict-dynamic'"], + 'style-src': ["'self'"] + }; + const response = await resolve(request); + + const nonce = request.locals.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 `'strict-dynamic'` directive is optional but supported by Kit. If not using it you must allow `'self'`. + +The nonce placeholders can be toggled with the `kit.cspNonce` configuration option. diff --git a/documentation/docs/14-configuration.md b/documentation/docs/15-configuration.md similarity index 98% rename from documentation/docs/14-configuration.md rename to documentation/docs/15-configuration.md index 2b4691b1057e..a2fb3051b713 100644 --- a/documentation/docs/14-configuration.md +++ b/documentation/docs/15-configuration.md @@ -53,6 +53,7 @@ const config = { ssr: true, target: null, trailingSlash: 'never', + cspNonce: false, vite: () => ({}) }, @@ -219,6 +220,10 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs to rou > Ignoring trailing slashes is not recommended — the semantics of relative paths differ between the two cases (`./y` from `/x` is `/y`, but from `/x/` is `/x/y`), and `/x` and `/x/` are treated as separate URLs which is harmful to SEO. If you use this option, ensure that you implement logic for conditionally adding or removing trailing slashes from `request.path` inside your [`handle`](#hooks-handle) function. +### cspNonce + +Enables or disables automatically adding [CSP Nonces](#content-security-policy) to your script and style tags. + ### vite A [Vite config object](https://vitejs.dev/config), or a function that returns one. You can pass [Vite and Rollup plugins](https://github.com/vitejs/awesome-vite#plugins) via [the `plugins` option](https://vitejs.dev/config/#plugins) to customize your build in advanced ways such as supporting image optimization, Tauri, WASM, Workbox, and more. SvelteKit will prevent you from setting certain build-related options since it depends on certain configuration values. From 6e723ed25787f552e89f765e2877a88609111df9 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Fri, 10 Sep 2021 18:11:54 +0200 Subject: [PATCH 04/41] Use CSP nonce generator shimmed by the adapter. --- packages/kit/src/core/build/index.js | 3 +-- packages/kit/src/runtime/server/index.js | 13 +++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index 4c621ac07529..da1516bdfd44 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -471,8 +471,7 @@ async function build_server( chunkFileNames: 'chunks/[name]-[hash].js', assetFileNames: 'assets/[name]-[hash][extname]' }, - preserveEntrySignatures: 'strict', - external: ['node:crypto'] + preserveEntrySignatures: 'strict' } }, plugins: [ diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 5b67573fee04..906e6a4d793b 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,3 +1,4 @@ +/* global generateCspNonce */ import { render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; @@ -7,7 +8,6 @@ import { lowercase_keys } from './utils.js'; import { hash } from '../hash.js'; import { get_single_valued_header } from '../../utils/http.js'; import { coalesce_to_error } from '../../utils/error.js'; -import { randomBytes } from 'node:crypto'; /** @type {import('@sveltejs/kit/ssr').Respond} */ export async function respond(incoming, options, state = {}) { @@ -33,7 +33,16 @@ export async function respond(incoming, options, state = {}) { } const headers = lowercase_keys(incoming.headers); - const nonce = randomBytes(32).toString('base64'); + let nonce; + try { + // generateCspNonce is not defined during prerender, only at runtime. + // TODO: We should probably differentiate between "missing because this is prerender" and "missing because the adapter is faulty". + nonce = options.cspNonce ? generateCspNonce() : undefined; + } catch (e) { + if (!(e instanceof ReferenceError)) { + throw e; + } + } const request = { ...incoming, headers, From ed86bab973557d97fe98fa9a9bcc72964a0227f3 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Fri, 10 Sep 2021 18:13:04 +0200 Subject: [PATCH 05/41] Add CspNonceGenerator shim to adapter-node. --- packages/adapter-node/src/shims.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js index 839b2dd5b108..dba048ff8320 100644 --- a/packages/adapter-node/src/shims.js +++ b/packages/adapter-node/src/shims.js @@ -1,6 +1,9 @@ import { createRequire } from 'module'; +import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; +export const generateCspNonce = () => randomBytes(16).toString('base64'); + // esbuild automatically renames "require" // So we still have to use Object.defineProperty here Object.defineProperty(globalThis, 'require', { From 8003b51bdaf3c5f6ebc14efa5a8303730accf118 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Fri, 10 Sep 2021 18:36:40 +0200 Subject: [PATCH 06/41] Add CspNonceGenerator shim to dev. --- packages/kit/src/core/dev/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 67f7cbef0cbc..de903f57018a 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -20,6 +20,7 @@ import create_manifest_data from '../create_manifest_data/index.js'; import { getRawBody } from '../node/index.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; import { copy_assets, resolve_entry } from '../utils.js'; +import { randomBytes } from 'crypto'; import { coalesce_to_error } from '../../utils/error.js'; /** @typedef {{ cwd?: string, port: number, host?: string, https: boolean, config: import('types/config').ValidatedConfig }} Options */ @@ -29,6 +30,13 @@ import { coalesce_to_error } from '../../utils/error.js'; export function dev(opts) { __fetch_polyfill(); + Object.defineProperties(globalThis, { + generateCspNonce: { + value: () => randomBytes(16).toString('base64'), + enumerable: true + } + }); + return new Watcher(opts).init(); } From 57ade8825bfa1b2560c44b446613a8a7c4c4f06b Mon Sep 17 00:00:00 2001 From: Karlinator Date: Fri, 10 Sep 2021 18:49:22 +0200 Subject: [PATCH 07/41] Add typing for nonce generator. --- packages/kit/src/runtime/server/index.js | 3 +++ packages/kit/types/globals.d.ts | 1 + 2 files changed, 4 insertions(+) create mode 100644 packages/kit/types/globals.d.ts diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 906e6a4d793b..7e09ddff4edb 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -33,6 +33,9 @@ export async function respond(incoming, options, state = {}) { } const headers = lowercase_keys(incoming.headers); + /** + * @type {string | undefined} + */ let nonce; try { // generateCspNonce is not defined during prerender, only at runtime. diff --git a/packages/kit/types/globals.d.ts b/packages/kit/types/globals.d.ts new file mode 100644 index 000000000000..c643adacc02c --- /dev/null +++ b/packages/kit/types/globals.d.ts @@ -0,0 +1 @@ +declare const generateCspNonce: () => string; From e5c3f1872e7f2ec5fac475885e65058121bd342d Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Fri, 10 Sep 2021 21:58:16 +0200 Subject: [PATCH 08/41] Fix failing test case. --- packages/kit/src/core/dev/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index de903f57018a..53e64c1f78c2 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -30,11 +30,9 @@ import { coalesce_to_error } from '../../utils/error.js'; export function dev(opts) { __fetch_polyfill(); - Object.defineProperties(globalThis, { - generateCspNonce: { - value: () => randomBytes(16).toString('base64'), - enumerable: true - } + Object.defineProperty(globalThis, 'generateCspNonce', { + value: () => randomBytes(16).toString('base64'), + configurable: true }); return new Watcher(opts).init(); From e886aed7d58e153bf5522129cf8ac8bbd10b4e1c Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Fri, 10 Sep 2021 22:08:45 +0200 Subject: [PATCH 09/41] [docs] Apply suggestions from code review Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/14-content-security-policy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index f8734a3e0722..a4948192b766 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -4,7 +4,7 @@ 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 CSP Nonces into the html it generates. +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 CSP nonces into the HTML it generates. The nonce value is availiable to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: @@ -42,7 +42,7 @@ export async function handle ({ request, resolve }) => { 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'`. +Be warned: some other features of Svelte ([in particular CSS transitions and animations](https://github.com/sveltejs/svelte/issues/6662)) might run afoul of this Content Security Policy and require either rewriting to JS-based transitions or enabling `style-src: 'unsafe-inline'`. The `'strict-dynamic'` directive is optional but supported by Kit. If not using it you must allow `'self'`. From dec6a4c03d10b8cfd5bb2c64f5d1aeee48df32b2 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Fri, 10 Sep 2021 22:28:49 +0200 Subject: [PATCH 10/41] Add generateCspNonce shim to all adapters. --- packages/adapter-cloudflare-workers/files/shims.js | 1 + packages/adapter-netlify/files/shims.js | 3 +++ packages/adapter-vercel/files/shims.js | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 packages/adapter-cloudflare-workers/files/shims.js diff --git a/packages/adapter-cloudflare-workers/files/shims.js b/packages/adapter-cloudflare-workers/files/shims.js new file mode 100644 index 000000000000..bda2cf83f667 --- /dev/null +++ b/packages/adapter-cloudflare-workers/files/shims.js @@ -0,0 +1 @@ +export const generateCspNonce = () => btoa(crypto.getRandomValues(new Uint32Array(4))); diff --git a/packages/adapter-netlify/files/shims.js b/packages/adapter-netlify/files/shims.js index cd9f71d6863c..31aae7b9ef40 100644 --- a/packages/adapter-netlify/files/shims.js +++ b/packages/adapter-netlify/files/shims.js @@ -1 +1,4 @@ +import { randomBytes } from 'node:crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; + +export const generateCspNonce = () => randomBytes(16).toString('base64'); diff --git a/packages/adapter-vercel/files/shims.js b/packages/adapter-vercel/files/shims.js index cd9f71d6863c..31aae7b9ef40 100644 --- a/packages/adapter-vercel/files/shims.js +++ b/packages/adapter-vercel/files/shims.js @@ -1 +1,4 @@ +import { randomBytes } from 'node:crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; + +export const generateCspNonce = () => randomBytes(16).toString('base64'); From a557b47941ba29c848740af85202e1bedafba971 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Fri, 10 Sep 2021 22:34:18 +0200 Subject: [PATCH 11/41] Add changeset for adapters --- .changeset/twenty-garlics-build.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/twenty-garlics-build.md diff --git a/.changeset/twenty-garlics-build.md b/.changeset/twenty-garlics-build.md new file mode 100644 index 000000000000..4a6ebd4c363e --- /dev/null +++ b/.changeset/twenty-garlics-build.md @@ -0,0 +1,8 @@ +--- +'@sveltejs/adapter-cloudflare-workers': patch +'@sveltejs/adapter-netlify': patch +'@sveltejs/adapter-node': patch +'@sveltejs/adapter-vercel': patch +--- + +Add generateCspNonce shim. From 416d86ed642c31394db67cb135f2a08593697236 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Fri, 10 Sep 2021 22:49:43 +0200 Subject: [PATCH 12/41] Fix deendency error in adapters. --- packages/adapter-netlify/files/shims.js | 2 +- packages/adapter-vercel/files/shims.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/adapter-netlify/files/shims.js b/packages/adapter-netlify/files/shims.js index 31aae7b9ef40..15d764e7b2e8 100644 --- a/packages/adapter-netlify/files/shims.js +++ b/packages/adapter-netlify/files/shims.js @@ -1,4 +1,4 @@ -import { randomBytes } from 'node:crypto'; +import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; export const generateCspNonce = () => randomBytes(16).toString('base64'); diff --git a/packages/adapter-vercel/files/shims.js b/packages/adapter-vercel/files/shims.js index 31aae7b9ef40..15d764e7b2e8 100644 --- a/packages/adapter-vercel/files/shims.js +++ b/packages/adapter-vercel/files/shims.js @@ -1,4 +1,4 @@ -import { randomBytes } from 'node:crypto'; +import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; export const generateCspNonce = () => randomBytes(16).toString('base64'); From fc0fd836ef8d54c3bc9277d55a1ccfd986d73c7b Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Fri, 10 Sep 2021 22:57:22 +0200 Subject: [PATCH 13/41] Add error handling for missing generateCspNonce. --- packages/kit/src/runtime/server/index.js | 19 ++++++++++++------- .../kit/src/runtime/server/page/render.js | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 7e09ddff4edb..945a1807aa8e 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -37,15 +37,20 @@ export async function respond(incoming, options, state = {}) { * @type {string | undefined} */ let nonce; - try { - // generateCspNonce is not defined during prerender, only at runtime. - // TODO: We should probably differentiate between "missing because this is prerender" and "missing because the adapter is faulty". - nonce = options.cspNonce ? generateCspNonce() : undefined; - } catch (e) { - if (!(e instanceof ReferenceError)) { - throw e; + if (!state.prerender && options.cspNonce) { + try { + nonce = generateCspNonce(); + } catch (e) { + if (e instanceof ReferenceError) { + console.warn( + "`kit.cspNonce` is active, but this adapter doesn't seem to support it. Nonces will not be inserted." + ); + } else { + throw e; + } } } + const request = { ...incoming, headers, diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 689aac4df80f..03533e643220 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -33,7 +33,7 @@ export async function render_response({ const css = new Set(options.entry.css); const js = new Set(options.entry.js); const styles = new Set(); - nonce = options.cspNonce ? `nonce="${nonce}"` : ''; + nonce = options.cspNonce && nonce ? `nonce="${nonce}"` : ''; /** @type {Array<{ url: string, body: string, json: string }>} */ const serialized_data = []; From 03579dadfa4bcde1d3ffc9a804c8b20b344e819c Mon Sep 17 00:00:00 2001 From: Karlinator Date: Fri, 10 Sep 2021 23:59:05 +0200 Subject: [PATCH 14/41] Fix which tags get nonces. --- packages/kit/src/runtime/server/page/render.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 03533e643220..3b67782cb858 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -101,13 +101,13 @@ 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(css).map((dep) => ``) + ...Array.from(css).map((dep) => ``) ].join('\n\t\t'); /** @type {string} */ @@ -115,7 +115,7 @@ export async function render_response({ if (options.amp) { init = ` - + `; init += options.service_worker From 3d4ac9ce9d3a01aa2b11fecd2a2c38917a954b5a Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Sat, 11 Sep 2021 00:01:23 +0200 Subject: [PATCH 15/41] Fix lint error. --- packages/kit/src/runtime/server/page/render.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 3b67782cb858..8e8f8ea1e834 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -101,9 +101,7 @@ 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) => ``), From 3ee92813dc3f5d55972743b2ad8f618e47673801 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sat, 11 Sep 2021 00:26:05 +0200 Subject: [PATCH 16/41] [docs] fix syntax error in CSP code example. --- documentation/docs/14-content-security-policy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index a4948192b766..5cf1e5e5e65a 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -9,7 +9,7 @@ However, SvelteKit also requires some small pieces of inline JavaScript in order The nonce value is availiable to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: ```javascript -export async function handle ({ request, resolve }) => { +export async function handle ({ request, resolve }) { const directives = { 'default-src': ["'self'", 'static.someotherdomain.com'], 'script-src': ["'strict-dynamic'"], From 7c137a1f2cde4cc60fc123e027826a6d5c7f8cae Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sun, 12 Sep 2021 01:48:10 +0200 Subject: [PATCH 17/41] Change adapter nonce API to be more generic. --- packages/adapter-cloudflare-workers/files/shims.js | 2 +- packages/adapter-netlify/files/shims.js | 2 +- packages/adapter-node/src/shims.js | 2 +- packages/adapter-vercel/files/shims.js | 2 +- packages/kit/src/runtime/server/index.js | 4 ++-- packages/kit/types/globals.d.ts | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/adapter-cloudflare-workers/files/shims.js b/packages/adapter-cloudflare-workers/files/shims.js index bda2cf83f667..c9305418f9a8 100644 --- a/packages/adapter-cloudflare-workers/files/shims.js +++ b/packages/adapter-cloudflare-workers/files/shims.js @@ -1 +1 @@ -export const generateCspNonce = () => btoa(crypto.getRandomValues(new Uint32Array(4))); +export const generateRandomString = (bytes) => btoa(crypto.getRandomValues(new Uint8Array(bytes))); diff --git a/packages/adapter-netlify/files/shims.js b/packages/adapter-netlify/files/shims.js index 15d764e7b2e8..8ba825c1bf03 100644 --- a/packages/adapter-netlify/files/shims.js +++ b/packages/adapter-netlify/files/shims.js @@ -1,4 +1,4 @@ import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; -export const generateCspNonce = () => randomBytes(16).toString('base64'); +export const generateRandomString = (bytes) => randomBytes(bytes).toString('base64'); diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js index dba048ff8320..205d0a698667 100644 --- a/packages/adapter-node/src/shims.js +++ b/packages/adapter-node/src/shims.js @@ -2,7 +2,7 @@ import { createRequire } from 'module'; import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; -export const generateCspNonce = () => randomBytes(16).toString('base64'); +export const generateRandomString = (bytes) => randomBytes(bytes).toString('base64'); // esbuild automatically renames "require" // So we still have to use Object.defineProperty here diff --git a/packages/adapter-vercel/files/shims.js b/packages/adapter-vercel/files/shims.js index 15d764e7b2e8..8ba825c1bf03 100644 --- a/packages/adapter-vercel/files/shims.js +++ b/packages/adapter-vercel/files/shims.js @@ -1,4 +1,4 @@ import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; -export const generateCspNonce = () => randomBytes(16).toString('base64'); +export const generateRandomString = (bytes) => randomBytes(bytes).toString('base64'); diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 945a1807aa8e..a16b457035af 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,4 +1,4 @@ -/* global generateCspNonce */ +/* global generateRandomString */ import { render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; @@ -39,7 +39,7 @@ export async function respond(incoming, options, state = {}) { let nonce; if (!state.prerender && options.cspNonce) { try { - nonce = generateCspNonce(); + nonce = generateRandomString(16); } catch (e) { if (e instanceof ReferenceError) { console.warn( diff --git a/packages/kit/types/globals.d.ts b/packages/kit/types/globals.d.ts index c643adacc02c..284b099d35d0 100644 --- a/packages/kit/types/globals.d.ts +++ b/packages/kit/types/globals.d.ts @@ -1 +1 @@ -declare const generateCspNonce: () => string; +declare const generateRandomString: (bytes: number) => string; From c98de8a587d7ab588e347455e8c508642932c02d Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sun, 12 Sep 2021 02:08:26 +0200 Subject: [PATCH 18/41] [docs] Document adapter changes required by #2394 --- documentation/docs/10-adapters.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/docs/10-adapters.md b/documentation/docs/10-adapters.md index 27212513f0e5..75f36678d3f6 100644 --- a/documentation/docs/10-adapters.md +++ b/documentation/docs/10-adapters.md @@ -92,6 +92,7 @@ Within the `adapt` method, there are a number of things that an adapter should d - Imports `init` and `render` from `.svelte-kit/output/server/app.js` - Calls `init`, which configures the app - Listens for requests from the platform, converts them to a a [SvelteKit request](#hooks-handle), calls the `render` function to generate a [SvelteKit response](#hooks-handle) and responds with it + - If config.kit.cspNonce is set, it should also generate a base64 cryptographically secure random string with at least 128 bits of entropy for use as a nonce and supply it in the render call. This must be unique for every request. Most platforms support either Node's crypto module or the Web Crypto API. - Globally shims `fetch` to work on the target platform, if necessary. SvelteKit provides a `@sveltejs/kit/install-fetch` helper for platforms that can use `node-fetch` - Bundle the output to avoid needing to install dependencies on the target platform, if desired - Call `utils.prerender` From bcbda4547c02d9354514dac16ca30c11c037231a Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sun, 12 Sep 2021 02:08:44 +0200 Subject: [PATCH 19/41] [docs] Improve CSP hook example. --- documentation/docs/14-content-security-policy.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index 5cf1e5e5e65a..a7c2c0277348 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -17,6 +17,10 @@ export async function handle ({ request, resolve }) { }; const response = await resolve(request); + if (response.headers['content-type'] !== 'text/html') { + return response + } + const nonce = request.locals.nonce; directives['script-src'].push(`'nonce-${nonce}'`); From 449a456e2b314f28a0ed49c630fd84eafed350b6 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sun, 12 Sep 2021 22:49:03 +0200 Subject: [PATCH 20/41] Fix injecting the shims. --- packages/adapter-cloudflare-workers/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 91909812030d..96398b1c54b6 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -3,6 +3,7 @@ import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; import { fileURLToPath } from 'url'; +import { join } from 'path'; /** * @typedef {import('esbuild').BuildOptions} BuildOptions @@ -39,6 +40,7 @@ export default function (options) { entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], outfile: `${entrypoint}/index.js`, bundle: true, + inject: [join(files, 'shims.js')], target: 'es2020', platform: 'browser' }; From d873cacf397700b4c82a760e9a592d0ada20de50 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 13 Sep 2021 16:17:13 +0200 Subject: [PATCH 21/41] Change nonce to be generated/supplied by adapter --- .../adapter-cloudflare-workers/files/entry.js | 4 +++- packages/adapter-cloudflare-workers/index.js | 11 +++++++---- packages/adapter-netlify/files/entry.js | 5 ++++- packages/adapter-netlify/files/shims.js | 3 --- packages/adapter-netlify/index.js | 5 ++++- packages/adapter-node/index.js | 3 ++- packages/adapter-node/src/kit-middleware.js | 6 +++++- packages/adapter-node/src/shims.js | 3 --- packages/adapter-vercel/files/entry.js | 5 ++++- packages/adapter-vercel/files/shims.js | 3 --- packages/adapter-vercel/index.js | 7 +++++-- packages/kit/src/core/dev/index.js | 8 ++------ packages/kit/src/runtime/server/index.js | 17 +++++------------ packages/kit/types/app.d.ts | 1 + packages/kit/types/globals.d.ts | 1 - 15 files changed, 42 insertions(+), 40 deletions(-) delete mode 100644 packages/kit/types/globals.d.ts diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index f7a7e397a620..17e8b9114a83 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,3 +1,4 @@ +/* global GENERATE_NONCES */ // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler'; @@ -35,7 +36,8 @@ async function handle(event) { query: request_url.searchParams, rawBody: await read(request), headers: Object.fromEntries(request.headers), - method: request.method + method: request.method, + nonce: GENERATE_NONCES && btoa(crypto.getRandomValues(new Uint32Array(2))) }); if (rendered) { diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 96398b1c54b6..106ca80b2e4b 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -1,9 +1,8 @@ -import fs from 'fs'; +import fs, { writeFileSync } from 'fs'; import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; import { fileURLToPath } from 'url'; -import { join } from 'path'; /** * @typedef {import('esbuild').BuildOptions} BuildOptions @@ -14,7 +13,7 @@ export default function (options) { return { name: '@sveltejs/adapter-cloudflare-workers', - async adapt({ utils }) { + async adapt({ utils, config }) { const { site } = validate_config(utils); const bucket = site.bucket; @@ -35,12 +34,16 @@ export default function (options) { utils.log.minor('Generating worker...'); utils.copy(`${files}/entry.js`, '.svelte-kit/cloudflare-workers/entry.js'); + writeFileSync('${files}/nofig.js', `export const generateNonces = ${config.kit.cspNonce}`); + /** @type {BuildOptions} */ const default_options = { entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], outfile: `${entrypoint}/index.js`, bundle: true, - inject: [join(files, 'shims.js')], + define: { + GENERATE_NONCES: config.kit.cspNonce.toString() // gets turned back into a boolean by esbuild + }, target: 'es2020', platform: 'browser' }; diff --git a/packages/adapter-netlify/files/entry.js b/packages/adapter-netlify/files/entry.js index ebf337948a90..4a29f9b767e0 100644 --- a/packages/adapter-netlify/files/entry.js +++ b/packages/adapter-netlify/files/entry.js @@ -1,3 +1,5 @@ +/* global GENERATE_NONCES */ +import { randomBytes } from 'crypto'; // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; @@ -16,7 +18,8 @@ export async function handler(event) { headers, path, query, - rawBody + rawBody, + nonce: GENERATE_NONCES && randomBytes(16).toString('base64') }); if (!rendered) { diff --git a/packages/adapter-netlify/files/shims.js b/packages/adapter-netlify/files/shims.js index 8ba825c1bf03..cd9f71d6863c 100644 --- a/packages/adapter-netlify/files/shims.js +++ b/packages/adapter-netlify/files/shims.js @@ -1,4 +1 @@ -import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; - -export const generateRandomString = (bytes) => randomBytes(bytes).toString('base64'); diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js index 2368d0f2d1cd..cfdc5c499fa6 100644 --- a/packages/adapter-netlify/index.js +++ b/packages/adapter-netlify/index.js @@ -13,7 +13,7 @@ export default function (options) { return { name: '@sveltejs/adapter-netlify', - async adapt({ utils }) { + async adapt({ utils, config }) { // "build" is the default publish directory when Netlify detects SvelteKit const publish = get_publish_directory(utils) || 'build'; @@ -34,6 +34,9 @@ export default function (options) { outfile: '.netlify/functions-internal/__render.js', bundle: true, inject: [join(files, 'shims.js')], + define: { + GENERATE_NONCES: config.kit.cspNonce.toString() // gets turned back into a boolean by esbuild + }, platform: 'node' }; diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js index 3d2b258fb6a7..a0cc45dfa21b 100644 --- a/packages/adapter-node/index.js +++ b/packages/adapter-node/index.js @@ -69,7 +69,8 @@ export default function ({ target: 'node14', inject: [join(files, 'shims.js')], define: { - APP_DIR: `"/${config.kit.appDir}/"` + APP_DIR: `"/${config.kit.appDir}/"`, + GENERATE_NONCES: config.kit.cspNonce.toString() // gets turned back into a boolean by esbuild } }; const build_options = esbuild_config ? await esbuild_config(defaultOptions) : defaultOptions; diff --git a/packages/adapter-node/src/kit-middleware.js b/packages/adapter-node/src/kit-middleware.js index bfa4a3429e48..4ef37646234b 100644 --- a/packages/adapter-node/src/kit-middleware.js +++ b/packages/adapter-node/src/kit-middleware.js @@ -1,4 +1,6 @@ +/* global GENERATE_NONCES */ import { getRawBody } from '@sveltejs/kit/node'; +import { randomBytes } from 'crypto'; /** * @return {import('polka').Middleware} @@ -29,7 +31,9 @@ export function create_kit_middleware({ render }) { headers: req.headers, // TODO: what about repeated headers, i.e. string[] path: parsed.pathname, query: parsed.searchParams, - rawBody: body + rawBody: body, + // @ts-ignore + nonce: GENERATE_NONCES && randomBytes(16).toString('base64') }); if (rendered) { diff --git a/packages/adapter-node/src/shims.js b/packages/adapter-node/src/shims.js index 205d0a698667..839b2dd5b108 100644 --- a/packages/adapter-node/src/shims.js +++ b/packages/adapter-node/src/shims.js @@ -1,9 +1,6 @@ import { createRequire } from 'module'; -import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; -export const generateRandomString = (bytes) => randomBytes(bytes).toString('base64'); - // esbuild automatically renames "require" // So we still have to use Object.defineProperty here Object.defineProperty(globalThis, 'require', { diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index 35d6e3170a7c..461f2d4e3a50 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -1,4 +1,6 @@ +/* global GENERATE_NONCES */ import { getRawBody } from '@sveltejs/kit/node'; +import { randomBytes } from 'crypto'; // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; @@ -22,7 +24,8 @@ export default async (req, res) => { headers: req.headers, path: pathname, query: searchParams, - rawBody: body + rawBody: body, + nonce: GENERATE_NONCES && randomBytes(16).toString('hex') }); if (rendered) { diff --git a/packages/adapter-vercel/files/shims.js b/packages/adapter-vercel/files/shims.js index 8ba825c1bf03..cd9f71d6863c 100644 --- a/packages/adapter-vercel/files/shims.js +++ b/packages/adapter-vercel/files/shims.js @@ -1,4 +1 @@ -import { randomBytes } from 'crypto'; export { fetch, Response, Request, Headers } from '@sveltejs/kit/install-fetch'; - -export const generateRandomString = (bytes) => randomBytes(bytes).toString('base64'); diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js index d5c53689f143..a7ee72737dd9 100644 --- a/packages/adapter-vercel/index.js +++ b/packages/adapter-vercel/index.js @@ -12,7 +12,7 @@ export default function (options) { return { name: '@sveltejs/adapter-vercel', - async adapt({ utils }) { + async adapt({ utils, config }) { const dir = '.vercel_build_output'; utils.rimraf(dir); @@ -37,7 +37,10 @@ export default function (options) { outfile: join(dirs.lambda, 'index.js'), bundle: true, inject: [join(files, 'shims.js')], - platform: 'node' + platform: 'node', + define: { + GENERATE_NONCES: config.kit.cspNonce.toString() // gets turned back into a boolean by esbuild + } }; const build_options = diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 53e64c1f78c2..a4b6eb10ed7e 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -30,11 +30,6 @@ import { coalesce_to_error } from '../../utils/error.js'; export function dev(opts) { __fetch_polyfill(); - Object.defineProperty(globalThis, 'generateCspNonce', { - value: () => randomBytes(16).toString('base64'), - configurable: true - }); - return new Watcher(opts).init(); } @@ -391,7 +386,8 @@ async function create_plugin(config, dir, cwd, get_manifest) { host, path: parsed.pathname.replace(config.kit.paths.base, ''), query: parsed.searchParams, - rawBody: body + rawBody: body, + nonce: config.kit.cspNonce && randomBytes(16).toString('base64') }, { amp: config.kit.amp, diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index a16b457035af..4b13c2d7ee33 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -1,4 +1,3 @@ -/* global generateRandomString */ import { render_endpoint } from './endpoint.js'; import { render_page } from './page/index.js'; import { render_response } from './page/render.js'; @@ -38,17 +37,11 @@ export async function respond(incoming, options, state = {}) { */ let nonce; if (!state.prerender && options.cspNonce) { - try { - nonce = generateRandomString(16); - } catch (e) { - if (e instanceof ReferenceError) { - console.warn( - "`kit.cspNonce` is active, but this adapter doesn't seem to support it. Nonces will not be inserted." - ); - } else { - throw e; - } - } + incoming.nonce + ? (nonce = incoming.nonce) + : console.warn( + '`kit.cspNonce` is active, but the adapter did not provide one. Nonces will not be inserted.' + ); } const request = { diff --git a/packages/kit/types/app.d.ts b/packages/kit/types/app.d.ts index 0fbe02bd9d7f..cf8ae5164e0a 100644 --- a/packages/kit/types/app.d.ts +++ b/packages/kit/types/app.d.ts @@ -18,4 +18,5 @@ export interface IncomingRequest { query: URLSearchParams; headers: RequestHeaders; rawBody: RawBody; + nonce?: string | false; } diff --git a/packages/kit/types/globals.d.ts b/packages/kit/types/globals.d.ts deleted file mode 100644 index 284b099d35d0..000000000000 --- a/packages/kit/types/globals.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare const generateRandomString: (bytes: number) => string; From ecb23aa1c60a42fff09c060e14e1f22fb3715a90 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 13 Sep 2021 16:46:49 +0200 Subject: [PATCH 22/41] Add nonces to preview server --- packages/kit/src/core/preview/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js index 5b1568ecc78b..e204bb60f134 100644 --- a/packages/kit/src/core/preview/index.js +++ b/packages/kit/src/core/preview/index.js @@ -7,6 +7,7 @@ import { pathToFileURL } from 'url'; import { getRawBody } from '../node/index.js'; import { __fetch_polyfill } from '../../install-fetch.js'; import { SVELTE_KIT, SVELTE_KIT_ASSETS } from '../constants.js'; +import { randomBytes } from 'crypto'; /** @param {string} dir */ const mutable = (dir) => @@ -96,7 +97,8 @@ export async function preview({ headers: /** @type {import('types/helper').RequestHeaders} */ (req.headers), path: parsed.pathname.replace(config.kit.paths.base, ''), query: parsed.searchParams, - rawBody: body + rawBody: body, + nonce: config.kit.cspNonce && randomBytes(16).toString('base64') })); if (rendered) { From d906254022d22fd52a733e6a7fa87d9774153a34 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 13 Sep 2021 17:04:43 +0200 Subject: [PATCH 23/41] Fix test case --- packages/adapter-node/tests/smoke.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adapter-node/tests/smoke.js b/packages/adapter-node/tests/smoke.js index ed7698959b2b..08e1d52bc5d0 100644 --- a/packages/adapter-node/tests/smoke.js +++ b/packages/adapter-node/tests/smoke.js @@ -6,6 +6,7 @@ import polka from 'polka'; const { PORT = 3000 } = process.env; const DEFAULT_SERVER_OPTS = { render: () => {} }; +globalThis.GENERATE_NONCES = true; // mock. Esbuild inserts this, but we don't esbuild before tests async function startServer(opts = DEFAULT_SERVER_OPTS) { return new Promise((fulfil, reject) => { From c9c973699da3e2c61387b0395d4fa688bd6670de Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Tue, 14 Sep 2021 19:02:44 +0200 Subject: [PATCH 24/41] Update cahngeset message for adapters. --- .changeset/twenty-garlics-build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/twenty-garlics-build.md b/.changeset/twenty-garlics-build.md index 4a6ebd4c363e..4435c48571ff 100644 --- a/.changeset/twenty-garlics-build.md +++ b/.changeset/twenty-garlics-build.md @@ -5,4 +5,4 @@ '@sveltejs/adapter-vercel': patch --- -Add generateCspNonce shim. +Add support for generating CSP nonces when `kit.cspNonce` is set. From 3a2b75676db5ba7b87ac1030da9cb02ae007b593 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Tue, 21 Sep 2021 01:40:52 +0200 Subject: [PATCH 25/41] Remove disused file. --- packages/adapter-cloudflare-workers/files/shims.js | 1 - 1 file changed, 1 deletion(-) delete mode 100644 packages/adapter-cloudflare-workers/files/shims.js diff --git a/packages/adapter-cloudflare-workers/files/shims.js b/packages/adapter-cloudflare-workers/files/shims.js deleted file mode 100644 index c9305418f9a8..000000000000 --- a/packages/adapter-cloudflare-workers/files/shims.js +++ /dev/null @@ -1 +0,0 @@ -export const generateRandomString = (bytes) => btoa(crypto.getRandomValues(new Uint8Array(bytes))); From 88269339f3e28227eed23c76cab602d69fb46dd8 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 18 Oct 2021 21:43:54 +0200 Subject: [PATCH 26/41] Implement suggestion from code review. --- packages/adapter-cloudflare-workers/files/entry.js | 3 +-- packages/adapter-cloudflare-workers/index.js | 4 +--- packages/adapter-netlify/files/entry.js | 3 +-- packages/adapter-node/src/kit-middleware.js | 3 +-- packages/adapter-vercel/files/entry.js | 3 +-- packages/kit/src/core/build/index.js | 2 +- packages/kit/src/runtime/server/index.js | 6 ++---- packages/kit/src/runtime/server/page/render.js | 2 +- packages/kit/types/internal.d.ts | 2 +- 9 files changed, 10 insertions(+), 18 deletions(-) diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index 17e8b9114a83..c95dddeb72ed 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -1,4 +1,3 @@ -/* global GENERATE_NONCES */ // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; import { getAssetFromKV, NotFoundError } from '@cloudflare/kv-asset-handler'; @@ -37,7 +36,7 @@ async function handle(event) { rawBody: await read(request), headers: Object.fromEntries(request.headers), method: request.method, - nonce: GENERATE_NONCES && btoa(crypto.getRandomValues(new Uint32Array(2))) + nonce: globalThis.GENERATE_NONCES && btoa(crypto.getRandomValues(new Uint32Array(2))) }); if (rendered) { diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 106ca80b2e4b..f03485541df7 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -1,4 +1,4 @@ -import fs, { writeFileSync } from 'fs'; +import fs from 'fs'; import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; @@ -34,8 +34,6 @@ export default function (options) { utils.log.minor('Generating worker...'); utils.copy(`${files}/entry.js`, '.svelte-kit/cloudflare-workers/entry.js'); - writeFileSync('${files}/nofig.js', `export const generateNonces = ${config.kit.cspNonce}`); - /** @type {BuildOptions} */ const default_options = { entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], diff --git a/packages/adapter-netlify/files/entry.js b/packages/adapter-netlify/files/entry.js index 4a29f9b767e0..fe6389fd219c 100644 --- a/packages/adapter-netlify/files/entry.js +++ b/packages/adapter-netlify/files/entry.js @@ -1,4 +1,3 @@ -/* global GENERATE_NONCES */ import { randomBytes } from 'crypto'; // TODO hardcoding the relative location makes this brittle import { init, render } from '../output/server/app.js'; @@ -19,7 +18,7 @@ export async function handler(event) { path, query, rawBody, - nonce: GENERATE_NONCES && randomBytes(16).toString('base64') + nonce: globalThis.GENERATE_NONCES && randomBytes(16).toString('base64') }); if (!rendered) { diff --git a/packages/adapter-node/src/kit-middleware.js b/packages/adapter-node/src/kit-middleware.js index 4ef37646234b..ef3a79a34bb3 100644 --- a/packages/adapter-node/src/kit-middleware.js +++ b/packages/adapter-node/src/kit-middleware.js @@ -1,4 +1,3 @@ -/* global GENERATE_NONCES */ import { getRawBody } from '@sveltejs/kit/node'; import { randomBytes } from 'crypto'; @@ -33,7 +32,7 @@ export function create_kit_middleware({ render }) { query: parsed.searchParams, rawBody: body, // @ts-ignore - nonce: GENERATE_NONCES && randomBytes(16).toString('base64') + nonce: globalThis.GENERATE_NONCES && randomBytes(16).toString('base64') }); if (rendered) { diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index 461f2d4e3a50..db6bbbf15839 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -1,4 +1,3 @@ -/* global GENERATE_NONCES */ import { getRawBody } from '@sveltejs/kit/node'; import { randomBytes } from 'crypto'; @@ -25,7 +24,7 @@ export default async (req, res) => { path: pathname, query: searchParams, rawBody: body, - nonce: GENERATE_NONCES && randomBytes(16).toString('hex') + nonce: globalThis.GENERATE_NONCES && randomBytes(16).toString('hex') }); if (rendered) { diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js index da1516bdfd44..614b5cce7eb0 100644 --- a/packages/kit/src/core/build/index.js +++ b/packages/kit/src/core/build/index.js @@ -344,7 +344,7 @@ async function build_server( target: ${s(config.kit.target)}, template, trailing_slash: ${s(config.kit.trailingSlash)}, - cspNonce: ${s(config.kit.cspNonce)} + csp_nonce: ${s(config.kit.cspNonce)} }; } diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js index 4b13c2d7ee33..ca0b42621540 100644 --- a/packages/kit/src/runtime/server/index.js +++ b/packages/kit/src/runtime/server/index.js @@ -32,11 +32,9 @@ export async function respond(incoming, options, state = {}) { } const headers = lowercase_keys(incoming.headers); - /** - * @type {string | undefined} - */ + /** @type {string | undefined} */ let nonce; - if (!state.prerender && options.cspNonce) { + if (!state.prerender && options.csp_nonce) { incoming.nonce ? (nonce = incoming.nonce) : console.warn( diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 8e8f8ea1e834..eec4d558e2c4 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -33,7 +33,7 @@ export async function render_response({ const css = new Set(options.entry.css); const js = new Set(options.entry.js); const styles = new Set(); - nonce = options.cspNonce && nonce ? `nonce="${nonce}"` : ''; + nonce = options.csp_nonce && nonce ? `nonce="${nonce}"` : ''; /** @type {Array<{ url: string, body: string, json: string }>} */ const serialized_data = []; diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts index 87dff3663aad..53d64f2edba5 100644 --- a/packages/kit/types/internal.d.ts +++ b/packages/kit/types/internal.d.ts @@ -159,7 +159,7 @@ export interface SSRRenderOptions { target: string; template({ head, body }: { head: string; body: string }): string; trailing_slash: TrailingSlash; - cspNonce: boolean; + csp_nonce: boolean; } export interface SSRRenderState { From 18e08ca0012530e5a9c2dc1a0ce2567942e8703b Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 18 Oct 2021 21:52:55 +0200 Subject: [PATCH 27/41] [Docs] Suggestions from code review. --- .../docs/14-content-security-policy.md | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index a7c2c0277348..ae472b4a5008 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -6,27 +6,27 @@ At the moment, SvelteKit supports adding Content Security Policy via hooks. In e 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 CSP nonces into the HTML it generates. -The nonce value is availiable to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: +The nonce value is available to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: ```javascript export async function handle ({ request, resolve }) { - const directives = { - 'default-src': ["'self'", 'static.someotherdomain.com'], - 'script-src': ["'strict-dynamic'"], - 'style-src': ["'self'"] - }; const response = await resolve(request); if (response.headers['content-type'] !== 'text/html') { return response - } + } const nonce = request.locals.nonce; - directives['script-src'].push(`'nonce-${nonce}'`); - directives['style-src'].push(`'nonce-${nonce}'`); + const directives = { + 'default-src': ["'self'", 'static.someotherdomain.com'], + 'script-src': ["'strict-dynamic'", `'nonce-${nonce}'`], + 'style-src': ["'self'", `'nonce-${nonce}'`] + }; if (process.env.NODE_ENV === 'development') { + // Because of the way Vite performs hot reloads of stylesheets, + // 'unsafe-inline' is required in dev mode. directives['style-src'].push('unsafe-inline') } @@ -44,8 +44,6 @@ export async function handle ({ request, resolve }) { }; ``` -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](https://github.com/sveltejs/svelte/issues/6662)) might run afoul of this Content Security Policy and require either rewriting to JS-based transitions or enabling `style-src: 'unsafe-inline'`. The `'strict-dynamic'` directive is optional but supported by Kit. If not using it you must allow `'self'`. From 9ff68e6982150e1adbfe19984f5abcb15494637b Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Tue, 28 Sep 2021 17:15:25 +0200 Subject: [PATCH 28/41] [docs] Update documentation/docs/14-content-security-policy.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/14-content-security-policy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index ae472b4a5008..e096f18c452c 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -2,7 +2,7 @@ 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. +At the moment, SvelteKit supports adding Content Security Policy via hooks. In environments with a server-side 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 CSP nonces into the HTML it generates. From cd269aca73105ad4f128a6b94f3fddc75237f143 Mon Sep 17 00:00:00 2001 From: Karl Erik Hofseth Date: Tue, 28 Sep 2021 17:19:58 +0200 Subject: [PATCH 29/41] [docs] Apply suggestions from code review Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/14-content-security-policy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index e096f18c452c..11cba9fc105d 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -4,7 +4,7 @@ title: Content Security Policy At the moment, SvelteKit supports adding Content Security Policy via hooks. In environments with a server-side 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 CSP nonces into the HTML it generates. +However, SvelteKit also requires some small pieces of inline JavaScript for hydration. To avoid using `'unsafe-inline'` (which, as the name suggests, should be avoided), SvelteKit can be configured to inject CSP nonces into the HTML it generates. The nonce value is available to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: @@ -13,8 +13,8 @@ export async function handle ({ request, resolve }) { const response = await resolve(request); if (response.headers['content-type'] !== 'text/html') { - return response - } + return response; + } const nonce = request.locals.nonce; From 449bd519d0cf295c71782d9b477f959001749426 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Tue, 19 Oct 2021 20:26:24 +0200 Subject: [PATCH 30/41] Fix misnamed option. --- packages/kit/src/core/dev/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index a4b6eb10ed7e..e8e570834cec 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -513,7 +513,7 @@ async function create_plugin(config, dir, cwd, get_manifest) { return rendered; }, trailing_slash: config.kit.trailingSlash, - cspNonce: config.kit.cspNonce + csp_nonce: config.kit.cspNonce } ); From 4ec250f54e6b9aade93843b52e75a9c1e02d10c3 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Wed, 3 Nov 2021 16:04:41 +0100 Subject: [PATCH 31/41] Disable prerendering if nonces are to be generated --- packages/kit/src/core/adapt/prerender.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kit/src/core/adapt/prerender.js b/packages/kit/src/core/adapt/prerender.js index 97d47cb35142..5ba0dbfa89f7 100644 --- a/packages/kit/src/core/adapt/prerender.js +++ b/packages/kit/src/core/adapt/prerender.js @@ -96,7 +96,7 @@ const REDIRECT = 3; * @returns {Promise>} returns a promise that resolves to an array of paths corresponding to the files that have been prerendered. */ export async function prerender({ cwd, out, log, config, build_data, fallback, all }) { - if (!config.kit.prerender.enabled && !fallback) { + if ((!config.kit.prerender.enabled || config.kit.cspNonce) && !fallback) { return []; } @@ -288,7 +288,7 @@ export async function prerender({ cwd, out, log, config, build_data, fallback, a } } - if (config.kit.prerender.enabled) { + if (config.kit.prerender.enabled && !config.kit.cspNonce) { for (const entry of config.kit.prerender.entries) { if (entry === '*') { for (const entry of build_data.entries) { From a13c75875e804f7d9de275f61edde82936bf2fa6 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Wed, 3 Nov 2021 16:05:39 +0100 Subject: [PATCH 32/41] Fix adapter-node nonce generation --- packages/adapter-node/src/kit-middleware.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/adapter-node/src/kit-middleware.js b/packages/adapter-node/src/kit-middleware.js index ef3a79a34bb3..1a16c7ef6099 100644 --- a/packages/adapter-node/src/kit-middleware.js +++ b/packages/adapter-node/src/kit-middleware.js @@ -31,8 +31,9 @@ export function create_kit_middleware({ render }) { path: parsed.pathname, query: parsed.searchParams, rawBody: body, - // @ts-ignore - nonce: globalThis.GENERATE_NONCES && randomBytes(16).toString('base64') + nonce: + // @ts-ignore + /* eslint-disable-line no-undef */ GENERATE_NONCES && randomBytes(16).toString('base64') }); if (rendered) { From 389959876c6ca0ffffc260a3219bcc74d6a09db4 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Wed, 3 Nov 2021 16:06:23 +0100 Subject: [PATCH 33/41] Fix Cloudflare workers nonce generation --- packages/adapter-cloudflare-workers/files/entry.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/adapter-cloudflare-workers/files/entry.js b/packages/adapter-cloudflare-workers/files/entry.js index c95dddeb72ed..688d78f56d31 100644 --- a/packages/adapter-cloudflare-workers/files/entry.js +++ b/packages/adapter-cloudflare-workers/files/entry.js @@ -36,7 +36,9 @@ async function handle(event) { rawBody: await read(request), headers: Object.fromEntries(request.headers), method: request.method, - nonce: globalThis.GENERATE_NONCES && btoa(crypto.getRandomValues(new Uint32Array(2))) + nonce: + /* eslint-disable-line no-undef */ GENERATE_NONCES && + btoa(crypto.getRandomValues(new Uint32Array(2))) }); if (rendered) { From b2a35cc80ef1dbf30d32fb3a1bead996cf975796 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Wed, 3 Nov 2021 16:16:32 +0100 Subject: [PATCH 34/41] Fix netlify nonce generation --- packages/adapter-netlify/files/entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-netlify/files/entry.js b/packages/adapter-netlify/files/entry.js index fe6389fd219c..5c3c680c6c6f 100644 --- a/packages/adapter-netlify/files/entry.js +++ b/packages/adapter-netlify/files/entry.js @@ -18,7 +18,7 @@ export async function handler(event) { path, query, rawBody, - nonce: globalThis.GENERATE_NONCES && randomBytes(16).toString('base64') + nonce: /* eslint-disable-line no-undef */ GENERATE_NONCES && randomBytes(16).toString('base64') }); if (!rendered) { From 51a0ba16fba54bd4da98c4f298f2b4d199924aaf Mon Sep 17 00:00:00 2001 From: Karlinator Date: Wed, 3 Nov 2021 16:34:03 +0100 Subject: [PATCH 35/41] Fix Vercel adapter nonce generation --- packages/adapter-vercel/files/entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-vercel/files/entry.js b/packages/adapter-vercel/files/entry.js index db6bbbf15839..9db3b3b50ede 100644 --- a/packages/adapter-vercel/files/entry.js +++ b/packages/adapter-vercel/files/entry.js @@ -24,7 +24,7 @@ export default async (req, res) => { path: pathname, query: searchParams, rawBody: body, - nonce: globalThis.GENERATE_NONCES && randomBytes(16).toString('hex') + nonce: /* eslint-disable-line no-undef */ GENERATE_NONCES && randomBytes(16).toString('hex') }); if (rendered) { From eb081b6ce71226a22d38094e6a799b7cdaf62e7e Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 13 Dec 2021 14:45:21 +0100 Subject: [PATCH 36/41] [docs] clarify `cspNonce` disables prerendering --- .../docs/14-content-security-policy.md | 56 +++++++++---------- documentation/docs/15-configuration.md | 4 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/documentation/docs/14-content-security-policy.md b/documentation/docs/14-content-security-policy.md index 11cba9fc105d..e0aec9ae0e33 100644 --- a/documentation/docs/14-content-security-policy.md +++ b/documentation/docs/14-content-security-policy.md @@ -9,43 +9,43 @@ However, SvelteKit also requires some small pieces of inline JavaScript for hydr The nonce value is available to hooks as `request.locals.nonce`. A basic CSP handler hook might then look like this: ```javascript -export async function handle ({ request, resolve }) { - const response = await resolve(request); +export async function handle({ request, resolve }) { + const response = await resolve(request); - if (response.headers['content-type'] !== 'text/html') { - return response; + if (response.headers['content-type'] !== 'text/html') { + return response; } - const nonce = request.locals.nonce; + const nonce = request.locals.nonce; - const directives = { - 'default-src': ["'self'", 'static.someotherdomain.com'], - 'script-src': ["'strict-dynamic'", `'nonce-${nonce}'`], - 'style-src': ["'self'", `'nonce-${nonce}'`] + const directives = { + 'default-src': ["'self'", 'static.someotherdomain.com'], + 'script-src': ["'strict-dynamic'", `'nonce-${nonce}'`], + 'style-src': ["'self'", `'nonce-${nonce}'`] }; - if (process.env.NODE_ENV === 'development') { - // Because of the way Vite performs hot reloads of stylesheets, - // 'unsafe-inline' is required in dev mode. - 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 - } - }; -}; + if (process.env.NODE_ENV === 'development') { + // Because of the way Vite performs hot reloads of stylesheets, + // 'unsafe-inline' is required in dev mode. + 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 + } + }; +} ``` Be warned: some other features of Svelte ([in particular CSS transitions and animations](https://github.com/sveltejs/svelte/issues/6662)) might run afoul of this Content Security Policy and require either rewriting to JS-based transitions or enabling `style-src: 'unsafe-inline'`. The `'strict-dynamic'` directive is optional but supported by Kit. If not using it you must allow `'self'`. -The nonce placeholders can be toggled with the `kit.cspNonce` configuration option. +The nonce placeholders can be toggled with the `kit.cspNonce` configuration option. Since nonces must be uniquely generated for each request, this also disables prerendering. diff --git a/documentation/docs/15-configuration.md b/documentation/docs/15-configuration.md index a2fb3051b713..ff2430bbc961 100644 --- a/documentation/docs/15-configuration.md +++ b/documentation/docs/15-configuration.md @@ -56,7 +56,7 @@ const config = { cspNonce: false, vite: () => ({}) }, - + // SvelteKit uses vite-plugin-svelte. Its options can be provided directly here. // See the available options at https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md @@ -222,7 +222,7 @@ Whether to remove, append, or ignore trailing slashes when resolving URLs to rou ### cspNonce -Enables or disables automatically adding [CSP Nonces](#content-security-policy) to your script and style tags. +Enables or disables automatically adding [CSP Nonces](#content-security-policy) to your script and style tags. Will also disable prerendering when active. ### vite From c81dc3a6cb9672b23973c8c7e01968c4ac2556c5 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sun, 12 Sep 2021 01:48:10 +0200 Subject: [PATCH 37/41] Change adapter nonce API to be more generic. --- packages/kit/types/globals.d.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/kit/types/globals.d.ts diff --git a/packages/kit/types/globals.d.ts b/packages/kit/types/globals.d.ts new file mode 100644 index 000000000000..e69de29bb2d1 From d5cc05badb82883009a54c4eedaa60a8b9b7b713 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sun, 12 Sep 2021 22:49:03 +0200 Subject: [PATCH 38/41] Fix injecting the shims. --- packages/adapter-cloudflare-workers/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index f03485541df7..c71020e0bec0 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -3,6 +3,7 @@ import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; import { fileURLToPath } from 'url'; +import { join } from 'path'; /** * @typedef {import('esbuild').BuildOptions} BuildOptions From a7b80031e59fc72c6ce2f78e64f70b614c0bba8a Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 13 Sep 2021 16:17:13 +0200 Subject: [PATCH 39/41] Change nonce to be generated/supplied by adapter --- packages/adapter-cloudflare-workers/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index c71020e0bec0..106ca80b2e4b 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -1,9 +1,8 @@ -import fs from 'fs'; +import fs, { writeFileSync } from 'fs'; import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; import { fileURLToPath } from 'url'; -import { join } from 'path'; /** * @typedef {import('esbuild').BuildOptions} BuildOptions @@ -35,6 +34,8 @@ export default function (options) { utils.log.minor('Generating worker...'); utils.copy(`${files}/entry.js`, '.svelte-kit/cloudflare-workers/entry.js'); + writeFileSync('${files}/nofig.js', `export const generateNonces = ${config.kit.cspNonce}`); + /** @type {BuildOptions} */ const default_options = { entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], From 7e693d5430031f0afb959db9025f2671df184d07 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Mon, 18 Oct 2021 21:43:54 +0200 Subject: [PATCH 40/41] Implement suggestion from code review. --- packages/adapter-cloudflare-workers/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js index 106ca80b2e4b..f03485541df7 100644 --- a/packages/adapter-cloudflare-workers/index.js +++ b/packages/adapter-cloudflare-workers/index.js @@ -1,4 +1,4 @@ -import fs, { writeFileSync } from 'fs'; +import fs from 'fs'; import { execSync } from 'child_process'; import esbuild from 'esbuild'; import toml from '@iarna/toml'; @@ -34,8 +34,6 @@ export default function (options) { utils.log.minor('Generating worker...'); utils.copy(`${files}/entry.js`, '.svelte-kit/cloudflare-workers/entry.js'); - writeFileSync('${files}/nofig.js', `export const generateNonces = ${config.kit.cspNonce}`); - /** @type {BuildOptions} */ const default_options = { entryPoints: ['.svelte-kit/cloudflare-workers/entry.js'], From cf262711aa0d2e8801b06351946880b1342c4913 Mon Sep 17 00:00:00 2001 From: Karlinator Date: Sat, 8 Jan 2022 05:13:03 +0100 Subject: [PATCH 41/41] Add nonces to adapter-cloudflare. --- packages/adapter-cloudflare/files/worker.js | 5 +++-- packages/adapter-cloudflare/index.js | 5 ++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/adapter-cloudflare/files/worker.js b/packages/adapter-cloudflare/files/worker.js index 8378d9f5add7..f854c692011f 100644 --- a/packages/adapter-cloudflare/files/worker.js +++ b/packages/adapter-cloudflare/files/worker.js @@ -1,4 +1,4 @@ -/* global ASSETS */ +/* global ASSETS, GENERATE_NONCES */ import { init, render } from '../output/server/app.js'; init(); @@ -18,7 +18,8 @@ export default { query: url.searchParams || '', rawBody: await read(req), headers: Object.fromEntries(req.headers), - method: req.method + method: req.method, + nonce: GENERATE_NONCES && btoa(crypto.getRandomValues(new Uint32Array(2))) }); if (rendered) { diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js index c0fa58184f96..fdc143b11ccc 100644 --- a/packages/adapter-cloudflare/index.js +++ b/packages/adapter-cloudflare/index.js @@ -45,7 +45,10 @@ export default function (options = {}) { outfile: target_worker, allowOverwrite: true, format: 'esm', - bundle: true + bundle: true, + define: { + GENERATE_NONCES: config.kit.cspNonce.toString() + } }); } };