Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSP #3499

Merged
merged 62 commits into from
Jan 26, 2022
Merged

CSP #3499

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
12f2a09
add CSP types
Rich-Harris Jan 21, 2022
74a3432
add csp stuff to config
Rich-Harris Jan 21, 2022
39c376d
add csp to SSRRenderOptions
Rich-Harris Jan 22, 2022
a767f4c
lay some groundwork
Rich-Harris Jan 22, 2022
77c5665
fall back to default-src
Rich-Harris Jan 22, 2022
043d064
more stuff
Rich-Harris Jan 22, 2022
cae0b70
generate meta tags last
Rich-Harris Jan 22, 2022
32b485f
move CSP logic out into separate (and more testable) class
Rich-Harris Jan 24, 2022
a3e66c3
merge master
Rich-Harris Jan 24, 2022
24a4141
merge master -> csp
Rich-Harris Jan 24, 2022
e86d2f0
fixes
Rich-Harris Jan 25, 2022
c533bf8
fix
Rich-Harris Jan 25, 2022
a71d0d6
lint
Rich-Harris Jan 25, 2022
4d519c3
add test to show CSP headers are working
Rich-Harris Jan 25, 2022
750794e
test for <meta http-equiv> tags
Rich-Harris Jan 25, 2022
4aa71d8
lint
Rich-Harris Jan 25, 2022
a730297
polyfill web crypto API in node
Rich-Harris Jan 25, 2022
75e2d1d
add install-crypto module for node-a-like environments
Rich-Harris Jan 25, 2022
ff2e450
add relevant subset of sjcl
Rich-Harris Jan 25, 2022
003b119
start tidying up
Rich-Harris Jan 25, 2022
0e676fe
remove some unused code
Rich-Harris Jan 25, 2022
cc9aca8
move some stuff out of the prototype
Rich-Harris Jan 25, 2022
07f86fc
use a class
Rich-Harris Jan 25, 2022
3ec91c6
more tidying
Rich-Harris Jan 25, 2022
527bb33
more tidying
Rich-Harris Jan 25, 2022
e5cf81d
more tidying
Rich-Harris Jan 25, 2022
1f2015a
fix all type errors
Rich-Harris Jan 25, 2022
d226b5b
store init vector and hash key as typed arrays
Rich-Harris Jan 25, 2022
7dc289b
convert to closure
Rich-Harris Jan 25, 2022
b2f77bc
more tidying
Rich-Harris Jan 25, 2022
58a22a1
hoist block
Rich-Harris Jan 25, 2022
277658e
more tidying
Rich-Harris Jan 25, 2022
4517b3b
use textdecoder
Rich-Harris Jan 25, 2022
90de3b5
create textencoder once
Rich-Harris Jan 25, 2022
532471c
more tidying
Rich-Harris Jan 25, 2022
9b87c54
simplify further
Rich-Harris Jan 25, 2022
dfb2c5d
more tidying
Rich-Harris Jan 25, 2022
720ad91
more tidying
Rich-Harris Jan 25, 2022
1dc0a05
simplify
Rich-Harris Jan 25, 2022
2b3ca8f
radically simplify
Rich-Harris Jan 25, 2022
65be836
simplify further
Rich-Harris Jan 25, 2022
3126da0
more crypto stuff
Rich-Harris Jan 25, 2022
969a771
use node crypto module to generate hashes where possible
Rich-Harris Jan 25, 2022
6e31aa0
remove unnecessary awaits
Rich-Harris Jan 25, 2022
5916636
fix mutation bug
Rich-Harris Jan 25, 2022
17afa3e
trick esbuild
Rich-Harris Jan 25, 2022
cb0968e
windows fix, hopefully
Rich-Harris Jan 25, 2022
7587a35
add unsafe-inline styles in dev
Rich-Harris Jan 25, 2022
00da7e6
gah windows
Rich-Harris Jan 25, 2022
6964723
oops
Rich-Harris Jan 25, 2022
b917a06
change install_fetch back to __fetch_polyfill (ugh)
Rich-Harris Jan 26, 2022
8f46c8d
revert cosmetic changes
Rich-Harris Jan 26, 2022
3350872
one base64 implementation is probably enough
Rich-Harris Jan 26, 2022
9b67148
changeset
Rich-Harris Jan 26, 2022
12fc809
document CSP stuff
Rich-Harris Jan 26, 2022
fd66521
remove out of date comment
Rich-Harris Jan 26, 2022
6dd2255
add TODO to remove node crypto stuff eventually
Rich-Harris Jan 26, 2022
f605edf
start adding CSP unit tests
Rich-Harris Jan 26, 2022
04649bc
various fixes, suppress strict-dynamic in dev
Rich-Harris Jan 26, 2022
1f3f33c
always create a nonce if template needs it, regardless of mode
Rich-Harris Jan 26, 2022
6c41b3d
comment out strict-dynamic handling for now
Rich-Harris Jan 26, 2022
a1d5b97
lint
Rich-Harris Jan 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spicy-glasses-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Add CSP support
30 changes: 30 additions & 0 deletions documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ const config = {
adapter: null,
amp: false,
appDir: '_app',
csp: {
mode: 'auto',
directives: {
'default-src': undefined
// ...
}
},
files: {
assets: 'static',
hooks: 'src/hooks',
Expand Down Expand Up @@ -82,6 +89,29 @@ Enable [AMP](#amp) mode.

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 `/`.

### csp

An object containing zero or more of the following values:

- `mode` — 'hash', 'nonce' or 'auto'
- `directives` — an object of `[directive]: value[]` pairs.

[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...

```js
{
directives: {
'script-src': ['self']
}
}
```

...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.

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).

> 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.

### files

An object containing zero or more of the following `string` values:
Expand Down
17 changes: 10 additions & 7 deletions packages/kit/src/core/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,25 @@ import { s } from '../../utils/misc.js';

/**
* @param {{
* cwd: string;
* hooks: string;
* config: import('types/config').ValidatedConfig;
* has_service_worker: boolean;
* template: string;
* }} opts
* @returns
*/
const template = ({ cwd, config, hooks, has_service_worker }) => `
const app_template = ({ config, hooks, has_service_worker, template }) => `
import root from '__GENERATED__/root.svelte';
import { respond } from '${runtime}/server/index.js';
import { set_paths, assets, base } from '${runtime}/paths.js';
import { set_prerendering } from '${runtime}/env.js';
import * as user_hooks from ${s(hooks)};

const template = ({ head, body, assets }) => ${s(load_template(cwd, config))
const template = ({ head, body, assets, nonce }) => ${s(template)
.replace('%svelte.head%', '" + head + "')
.replace('%svelte.body%', '" + body + "')
.replace(/%svelte\.assets%/g, '" + assets + "')};
.replace(/%svelte\.assets%/g, '" + assets + "')
.replace(/%svelte\.nonce%/g, '" + nonce + "')};

let read = null;

Expand Down Expand Up @@ -60,6 +61,7 @@ export class App {

this.options = {
amp: ${config.kit.amp},
csp: ${s(config.kit.csp)},
dev: false,
floc: ${config.kit.floc},
get_stack: error => String(error), // for security
Expand Down Expand Up @@ -89,6 +91,7 @@ export class App {
router: ${s(config.kit.router)},
target: ${s(config.kit.target)},
template,
template_contains_nonce: ${template.includes('%svelte.nonce%')},
trailing_slash: ${s(config.kit.trailingSlash)}
};
}
Expand Down Expand Up @@ -172,11 +175,11 @@ export async function build_server(
// prettier-ignore
fs.writeFileSync(
input.app,
template({
cwd,
app_template({
config,
hooks: app_relative(hooks_file),
has_service_worker: service_worker_register && !!service_worker_entry_file
has_service_worker: service_worker_register && !!service_worker_entry_file,
template: load_template(cwd, config)
})
);

Expand Down
225 changes: 123 additions & 102 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,99 @@
import { join } from 'path';
import { fileURLToPath } from 'url';
import { test } from 'uvu';
import * as assert from 'uvu/assert';

import { remove_keys } from '../../utils/object.js';
import { validate_config } from './index.js';
import { validate_config, load_config } from './index.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = join(__filename, '..');

const get_defaults = (prefix = '') => ({
extensions: ['.svelte'],
kit: {
adapter: null,
amp: false,
appDir: '_app',
csp: {
mode: 'auto',
directives: {
'child-src': undefined,
'default-src': undefined,
'frame-src': undefined,
'worker-src': undefined,
'connect-src': undefined,
'font-src': undefined,
'img-src': undefined,
'manifest-src': undefined,
'media-src': undefined,
'object-src': undefined,
'prefetch-src': undefined,
'script-src': undefined,
'script-src-elem': undefined,
'script-src-attr': undefined,
'style-src': undefined,
'style-src-elem': undefined,
'style-src-attr': undefined,
'base-uri': undefined,
sandbox: undefined,
'form-action': undefined,
'frame-ancestors': undefined,
'navigate-to': undefined,
'report-uri': undefined,
'report-to': undefined,
'require-trusted-types-for': undefined,
'trusted-types': undefined,
'upgrade-insecure-requests': false,
'require-sri-for': undefined,
'block-all-mixed-content': false,
'plugin-types': undefined,
referrer: undefined
}
},
files: {
assets: join(prefix, 'static'),
hooks: join(prefix, 'src/hooks'),
lib: join(prefix, 'src/lib'),
routes: join(prefix, 'src/routes'),
serviceWorker: join(prefix, 'src/service-worker'),
template: join(prefix, 'src/app.html')
},
floc: false,
headers: undefined,
host: undefined,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
},
package: {
dir: 'package',
emitTypes: true
},
serviceWorker: {
register: true
},
paths: {
base: '',
assets: ''
},
prerender: {
concurrency: 1,
crawl: true,
enabled: true,
entries: ['*'],
force: undefined,
onError: 'fail',
pages: undefined
},
protocol: undefined,
router: true,
ssr: null,
target: null,
trailingSlash: 'never'
}
});

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

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

assert.equal(validated, {
extensions: ['.svelte'],
kit: {
adapter: null,
amp: false,
appDir: '_app',
files: {
assets: 'static',
hooks: 'src/hooks',
lib: 'src/lib',
routes: 'src/routes',
serviceWorker: 'src/service-worker',
template: 'src/app.html'
},
floc: false,
headers: undefined,
host: undefined,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
},
package: {
dir: 'package',
emitTypes: true
},
serviceWorker: {
register: true
},
paths: {
base: '',
assets: ''
},
prerender: {
concurrency: 1,
crawl: true,
enabled: true,
entries: ['*'],
force: undefined,
onError: 'fail',
pages: undefined
},
protocol: undefined,
router: true,
ssr: null,
target: null,
trailingSlash: 'never'
}
});
assert.equal(validated, get_defaults());
});

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

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

assert.equal(validated, {
extensions: ['.svelte'],
kit: {
adapter: null,
amp: false,
appDir: '_app',
files: {
assets: 'public',
hooks: 'src/hooks',
lib: 'src/lib',
routes: 'src/routes',
serviceWorker: 'src/service-worker',
template: 'src/app.html'
},
floc: false,
headers: undefined,
host: undefined,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
},
package: {
dir: 'package',
emitTypes: true
},
serviceWorker: {
register: true
},
paths: {
base: '',
assets: ''
},
prerender: {
concurrency: 1,
crawl: true,
enabled: true,
entries: ['*'],
force: undefined,
onError: 'fail',
pages: undefined
},
protocol: undefined,
router: true,
ssr: null,
target: null,
trailingSlash: 'never'
}
});
const config = get_defaults();
config.kit.files.assets = 'public';

assert.equal(validated, config);
});

test('fails if kit.appDir is blank', () => {
Expand Down Expand Up @@ -327,4 +323,29 @@ validate_paths(
}
);

test('load default config (esm)', async () => {
const cwd = join(__dirname, 'fixtures/default');

const config = await load_config({ cwd });
remove_keys(config, ([, v]) => typeof v === 'function');

assert.equal(config, get_defaults(cwd + '/'));
});

test('errors on loading config with incorrect default export', async () => {
let message = null;

try {
const cwd = join(__dirname, 'fixtures', 'export-string');
await load_config({ cwd });
} catch (/** @type {any} */ e) {
message = e.message;
}

assert.equal(
message,
'svelte.config.js must have a configuration object as its default export. See https://kit.svelte.dev/docs#configuration'
);
});

test.run();
Loading