Skip to content

Commit 1b68649

Browse files
authored
CSP (#3499)
* add CSP types * add csp stuff to config * add csp to SSRRenderOptions * lay some groundwork * fall back to default-src * more stuff * generate meta tags last * move CSP logic out into separate (and more testable) class * fixes * fix * lint * add test to show CSP headers are working * test for <meta http-equiv> tags * lint * polyfill web crypto API in node * add install-crypto module for node-a-like environments * add relevant subset of sjcl * start tidying up * remove some unused code * move some stuff out of the prototype * use a class * more tidying * more tidying * more tidying * fix all type errors * store init vector and hash key as typed arrays * convert to closure * more tidying * hoist block * more tidying * use textdecoder * create textencoder once * more tidying * simplify further * more tidying * more tidying * simplify * radically simplify * simplify further * more crypto stuff * use node crypto module to generate hashes where possible * remove unnecessary awaits * fix mutation bug * trick esbuild * windows fix, hopefully * add unsafe-inline styles in dev * gah windows * oops * change install_fetch back to __fetch_polyfill (ugh) * revert cosmetic changes * one base64 implementation is probably enough * changeset * document CSP stuff * remove out of date comment * add TODO to remove node crypto stuff eventually * start adding CSP unit tests * various fixes, suppress strict-dynamic in dev * always create a nonce if template needs it, regardless of mode * comment out strict-dynamic handling for now * lint
1 parent 2dc7774 commit 1b68649

File tree

25 files changed

+1246
-250
lines changed

25 files changed

+1246
-250
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
Add CSP support

documentation/docs/14-configuration.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ const config = {
1717
adapter: null,
1818
amp: false,
1919
appDir: '_app',
20+
csp: {
21+
mode: 'auto',
22+
directives: {
23+
'default-src': undefined
24+
// ...
25+
}
26+
},
2027
files: {
2128
assets: 'static',
2229
hooks: 'src/hooks',
@@ -82,6 +89,29 @@ Enable [AMP](#amp) mode.
8289

8390
The directory relative to `paths.assets` where the built JS and CSS (and imported assets) are served from. (The filenames therein contain content-based hashes, meaning they can be cached indefinitely). Must not start or end with `/`.
8491

92+
### csp
93+
94+
An object containing zero or more of the following values:
95+
96+
- `mode` — 'hash', 'nonce' or 'auto'
97+
- `directives` — an object of `[directive]: value[]` pairs.
98+
99+
[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) configuration. CSP helps to protect your users against cross-site scripting (XSS) attacks, by limiting the places resources can be loaded from. For example, a configuration like this...
100+
101+
```js
102+
{
103+
directives: {
104+
'script-src': ['self']
105+
}
106+
}
107+
```
108+
109+
...would prevent scripts loading from external sites. SvelteKit will augment the specified directives with nonces or hashes (depending on `mode`) for any inline styles and scripts it generates.
110+
111+
When pages are prerendered, the CSP header is added via a `<meta http-equiv>` tag (note that in this case, `frame-ancestors`, `report-uri` and `sandbox` directives will be ignored).
112+
113+
> When `mode` is `'auto'`, SvelteKit will use nonces for dynamically rendered pages and hashes for prerendered pages. Using nonces with prerendered pages is insecure and therefore forbiddem.
114+
85115
### files
86116

87117
An object containing zero or more of the following `string` values:

packages/kit/src/core/build/build_server.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,25 @@ import { s } from '../../utils/misc.js';
1111

1212
/**
1313
* @param {{
14-
* cwd: string;
1514
* hooks: string;
1615
* config: import('types/config').ValidatedConfig;
1716
* has_service_worker: boolean;
17+
* template: string;
1818
* }} opts
1919
* @returns
2020
*/
21-
const template = ({ cwd, config, hooks, has_service_worker }) => `
21+
const app_template = ({ config, hooks, has_service_worker, template }) => `
2222
import root from '__GENERATED__/root.svelte';
2323
import { respond } from '${runtime}/server/index.js';
2424
import { set_paths, assets, base } from '${runtime}/paths.js';
2525
import { set_prerendering } from '${runtime}/env.js';
2626
import * as user_hooks from ${s(hooks)};
2727
28-
const template = ({ head, body, assets }) => ${s(load_template(cwd, config))
28+
const template = ({ head, body, assets, nonce }) => ${s(template)
2929
.replace('%svelte.head%', '" + head + "')
3030
.replace('%svelte.body%', '" + body + "')
31-
.replace(/%svelte\.assets%/g, '" + assets + "')};
31+
.replace(/%svelte\.assets%/g, '" + assets + "')
32+
.replace(/%svelte\.nonce%/g, '" + nonce + "')};
3233
3334
let read = null;
3435
@@ -60,6 +61,7 @@ export class App {
6061
6162
this.options = {
6263
amp: ${config.kit.amp},
64+
csp: ${s(config.kit.csp)},
6365
dev: false,
6466
floc: ${config.kit.floc},
6567
get_stack: error => String(error), // for security
@@ -89,6 +91,7 @@ export class App {
8991
router: ${s(config.kit.router)},
9092
target: ${s(config.kit.target)},
9193
template,
94+
template_contains_nonce: ${template.includes('%svelte.nonce%')},
9295
trailing_slash: ${s(config.kit.trailingSlash)}
9396
};
9497
}
@@ -172,11 +175,11 @@ export async function build_server(
172175
// prettier-ignore
173176
fs.writeFileSync(
174177
input.app,
175-
template({
176-
cwd,
178+
app_template({
177179
config,
178180
hooks: app_relative(hooks_file),
179-
has_service_worker: service_worker_register && !!service_worker_entry_file
181+
has_service_worker: service_worker_register && !!service_worker_entry_file,
182+
template: load_template(cwd, config)
180183
})
181184
);
182185

packages/kit/src/core/config/index.spec.js

Lines changed: 123 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,99 @@
1+
import { join } from 'path';
2+
import { fileURLToPath } from 'url';
13
import { test } from 'uvu';
24
import * as assert from 'uvu/assert';
3-
45
import { remove_keys } from '../../utils/object.js';
5-
import { validate_config } from './index.js';
6+
import { validate_config, load_config } from './index.js';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = join(__filename, '..');
10+
11+
const get_defaults = (prefix = '') => ({
12+
extensions: ['.svelte'],
13+
kit: {
14+
adapter: null,
15+
amp: false,
16+
appDir: '_app',
17+
csp: {
18+
mode: 'auto',
19+
directives: {
20+
'child-src': undefined,
21+
'default-src': undefined,
22+
'frame-src': undefined,
23+
'worker-src': undefined,
24+
'connect-src': undefined,
25+
'font-src': undefined,
26+
'img-src': undefined,
27+
'manifest-src': undefined,
28+
'media-src': undefined,
29+
'object-src': undefined,
30+
'prefetch-src': undefined,
31+
'script-src': undefined,
32+
'script-src-elem': undefined,
33+
'script-src-attr': undefined,
34+
'style-src': undefined,
35+
'style-src-elem': undefined,
36+
'style-src-attr': undefined,
37+
'base-uri': undefined,
38+
sandbox: undefined,
39+
'form-action': undefined,
40+
'frame-ancestors': undefined,
41+
'navigate-to': undefined,
42+
'report-uri': undefined,
43+
'report-to': undefined,
44+
'require-trusted-types-for': undefined,
45+
'trusted-types': undefined,
46+
'upgrade-insecure-requests': false,
47+
'require-sri-for': undefined,
48+
'block-all-mixed-content': false,
49+
'plugin-types': undefined,
50+
referrer: undefined
51+
}
52+
},
53+
files: {
54+
assets: join(prefix, 'static'),
55+
hooks: join(prefix, 'src/hooks'),
56+
lib: join(prefix, 'src/lib'),
57+
routes: join(prefix, 'src/routes'),
58+
serviceWorker: join(prefix, 'src/service-worker'),
59+
template: join(prefix, 'src/app.html')
60+
},
61+
floc: false,
62+
headers: undefined,
63+
host: undefined,
64+
hydrate: true,
65+
inlineStyleThreshold: 0,
66+
methodOverride: {
67+
parameter: '_method',
68+
allowed: []
69+
},
70+
package: {
71+
dir: 'package',
72+
emitTypes: true
73+
},
74+
serviceWorker: {
75+
register: true
76+
},
77+
paths: {
78+
base: '',
79+
assets: ''
80+
},
81+
prerender: {
82+
concurrency: 1,
83+
crawl: true,
84+
enabled: true,
85+
entries: ['*'],
86+
force: undefined,
87+
onError: 'fail',
88+
pages: undefined
89+
},
90+
protocol: undefined,
91+
router: true,
92+
ssr: null,
93+
target: null,
94+
trailingSlash: 'never'
95+
}
96+
});
697

798
test('fills in defaults', () => {
899
const validated = validate_config({});
@@ -14,56 +105,7 @@ test('fills in defaults', () => {
14105

15106
remove_keys(validated, ([, v]) => typeof v === 'function');
16107

17-
assert.equal(validated, {
18-
extensions: ['.svelte'],
19-
kit: {
20-
adapter: null,
21-
amp: false,
22-
appDir: '_app',
23-
files: {
24-
assets: 'static',
25-
hooks: 'src/hooks',
26-
lib: 'src/lib',
27-
routes: 'src/routes',
28-
serviceWorker: 'src/service-worker',
29-
template: 'src/app.html'
30-
},
31-
floc: false,
32-
headers: undefined,
33-
host: undefined,
34-
hydrate: true,
35-
inlineStyleThreshold: 0,
36-
methodOverride: {
37-
parameter: '_method',
38-
allowed: []
39-
},
40-
package: {
41-
dir: 'package',
42-
emitTypes: true
43-
},
44-
serviceWorker: {
45-
register: true
46-
},
47-
paths: {
48-
base: '',
49-
assets: ''
50-
},
51-
prerender: {
52-
concurrency: 1,
53-
crawl: true,
54-
enabled: true,
55-
entries: ['*'],
56-
force: undefined,
57-
onError: 'fail',
58-
pages: undefined
59-
},
60-
protocol: undefined,
61-
router: true,
62-
ssr: null,
63-
target: null,
64-
trailingSlash: 'never'
65-
}
66-
});
108+
assert.equal(validated, get_defaults());
67109
});
68110

69111
test('errors on invalid values', () => {
@@ -123,56 +165,10 @@ test('fills in partial blanks', () => {
123165

124166
remove_keys(validated, ([, v]) => typeof v === 'function');
125167

126-
assert.equal(validated, {
127-
extensions: ['.svelte'],
128-
kit: {
129-
adapter: null,
130-
amp: false,
131-
appDir: '_app',
132-
files: {
133-
assets: 'public',
134-
hooks: 'src/hooks',
135-
lib: 'src/lib',
136-
routes: 'src/routes',
137-
serviceWorker: 'src/service-worker',
138-
template: 'src/app.html'
139-
},
140-
floc: false,
141-
headers: undefined,
142-
host: undefined,
143-
hydrate: true,
144-
inlineStyleThreshold: 0,
145-
methodOverride: {
146-
parameter: '_method',
147-
allowed: []
148-
},
149-
package: {
150-
dir: 'package',
151-
emitTypes: true
152-
},
153-
serviceWorker: {
154-
register: true
155-
},
156-
paths: {
157-
base: '',
158-
assets: ''
159-
},
160-
prerender: {
161-
concurrency: 1,
162-
crawl: true,
163-
enabled: true,
164-
entries: ['*'],
165-
force: undefined,
166-
onError: 'fail',
167-
pages: undefined
168-
},
169-
protocol: undefined,
170-
router: true,
171-
ssr: null,
172-
target: null,
173-
trailingSlash: 'never'
174-
}
175-
});
168+
const config = get_defaults();
169+
config.kit.files.assets = 'public';
170+
171+
assert.equal(validated, config);
176172
});
177173

178174
test('fails if kit.appDir is blank', () => {
@@ -327,4 +323,29 @@ validate_paths(
327323
}
328324
);
329325

326+
test('load default config (esm)', async () => {
327+
const cwd = join(__dirname, 'fixtures/default');
328+
329+
const config = await load_config({ cwd });
330+
remove_keys(config, ([, v]) => typeof v === 'function');
331+
332+
assert.equal(config, get_defaults(cwd + '/'));
333+
});
334+
335+
test('errors on loading config with incorrect default export', async () => {
336+
let message = null;
337+
338+
try {
339+
const cwd = join(__dirname, 'fixtures', 'export-string');
340+
await load_config({ cwd });
341+
} catch (/** @type {any} */ e) {
342+
message = e.message;
343+
}
344+
345+
assert.equal(
346+
message,
347+
'svelte.config.js must have a configuration object as its default export. See https://kit.svelte.dev/docs#configuration'
348+
);
349+
});
350+
330351
test.run();

0 commit comments

Comments
 (0)