Opens an external site in a new window
Mental Health Awareness Month
“Community”
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links
  • Profile
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links

SvelteKit Content Security Policy: CSP for XSS Protection # SvelteKit Content Security Policy: CSP for XSS Protection #

blurry low resolution placeholder image SvelteKit Content Security Policy
  1. Home Rodney Lab Home
  2. Blog Posts Rodney Lab Blog Posts
  3. SvelteKit SvelteKit Blog Posts
<PREVIOUS POST
NEXT POST >
LATEST POST >>

SvelteKit Content Security Policy: CSP for XSS Protection #

Updated 2 years ago
9 minute read
Gunning Fog Index: 6.1
2 comments
Content by Rodney
blurry low resolution placeholder image Author Image: Rodney from Rodney Lab
SHARE:

😕 What is Content Security Policy? #

Today we look at SvelteKit Content Security Policy. Content Security Policy is a set of meta you can send from your server to visitors’ browsers to help improve security. It is designed to reduce the cross site scripting (XSS) attack surface. At its core, the script directives help the browser identify foreign scripts which might have been injected by a malicious party. However, content security policy covers styles, images and other resources beyond scripts.

We see with scripts we can compute a cryptographic hash of the intended script on the server and send this with the page. By hashing the received script itself and comparing to the list of CSP hashes, the browser can potentially spot injected malicious scripts. We will see hashing is not the only choice and see when you might consider using the alternatives. SvelteKit got a patch in February which lets it automatically compute script hashes and inject a content security policy tag in the page head.

You should only attempt to update content security policy if you are confident you know what you are doing. It is possible to completely stop a site from rendering with the wrong policy.

🔬 What is our Focus? #

We will see why and how we can use the SvelteKit generated CSP meta tag to add an HTTP Content Security Policy header to a static site. As well as that, we also look at the configuration for deploying the site with headers to Netlify and Cloudflare Pages. We will use the SvelteKit MDsveX blog starter, though the approach should work well with other sites. This should all get us an A rating on SecurityHeaders.com for the site.

blurry low resolution placeholder image SvelteKit Content Security Policy: Screenshot shows summary of HTTP headers scan by Security Headers dot com with an A rating.
Screenshot: SvelteKit Content Security Policy: SecurityHeaders summary

⚙️ Configuration #

If you want to code along, then clone the SvelteKit MDsveX blog starter and install packages:

    
git clone https://github.com/rodneylab/sveltekit-blog-mdx.git sveltekit-content-security-policy
cd sveltekit-content-security-policy
pnpm install
pnpm run dev

We just need to update svelte.config.js to have it create the CSP meta for us:

svelte.config.js
javascript
    
1 /** @type {import('@sveltejs/kit').Config} */
2 import adapter from "@sveltejs/adapter-static";
3 import { mdsvex } from "mdsvex";
4 import preprocess from "svelte-preprocess";
5
6 const config = {
7 extensions: [".svelte", ".md", ".svelte.md"],
8 preprocess: [
9 mdsvex({ extensions: [".svelte.md", ".md", ".svx"] }),
10 preprocess({
11 scss: {
12 prependData: "@import 'src/lib/styles/variables.scss';",
13 },
14 }),
15 ],
16 kit: {
17 adapter: adapter({ precompress: true }),
18 csp: {
19 mode: "hash",
20 directives: { "script-src": ["self"] },
21 },
22 files: {
23 hooks: "src/hooks",
24 },
25 prerender: { default: true },
26 },
27 };
28
29 export default config;

We can set mode to hash, nonce or auto. hash will compute a SHA256 cryptographic hash of all scripts which SvelteKit generates in building the site. These scripts are later used by visitors’ browsers to sniff out foul play. Hashes are a good choice for static sites. This is because scripts are fixed on build and will not change until you rebuild the site.

With SSR sites, SvelteKit might generate a different script for each request. To avoid the extra overhead of computing a set of hashes for each request, an alternative is to use a nonce. The nonce is just a randomly generated string. We just add the nonce to each script tag and also include it in the CSP meta. Now the browser just checks the nonce in the script match the one in the meta. For this to work best, we need to generate a new random nonce with each request.

The third option, auto, simply chooses hash for prerendered content and nonce for anything else.

Alternative Configuration #

This configuration (above) is a little basic. You might want to be a bit more extensive configuration. In this case, it makes more sense to extract the configuration to a separate file, and you can update svelte.config.js like so:

svelte.config.js
javascript
    
1 /** @type {import('@sveltejs/kit').Config} */
2 import adapter from '@sveltejs/adapter-static';
3 import { mdsvex } from 'mdsvex';
4 import preprocess from 'svelte-preprocess';
5 import cspDirectives from './csp-directives.mjs';
6
7 const config = {
8 extensions: ['.svelte', '.md', '.svelte.md'],
9 preprocess: [
10 mdsvex({ extensions: ['.svelte.md', '.md', '.svx'] }),
11 preprocess({
12 scss: {
13 prependData: "@import 'src/lib/styles/variables.scss';",
14 },
15 }),
16 ],
17 kit: {
18 adapter: adapter({ precompress: true }),
19 csp: {
20 mode: 'hash',
21 directives: cspDirectives,
22 },
23 files: {
24 hooks: 'src/hooks',
25 },
26 prerender: { default: true },
27 },
28 };
29
30 export default config;

Here is one possible set of values you might use. Of course this will not match your use case, and you should determine a set of values which are suitable.

csp-directives.mjs — click to expand code.
csp-directives.mjs
javascript
    
1 const rootDomain = process.env.VITE_DOMAIN; // or your server IP for dev
2
3 const cspDirectives = {
4 'base-uri': ["'self'"],
5 'child-src': ["'self'"],
6 'connect-src': ["'self'", 'ws://localhost:*'],
7 // 'connect-src': ["'self'", 'ws://localhost:*', 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
8 'img-src': ["'self'", 'data:'],
9 'font-src': ["'self'", 'data:'],
10 'form-action': ["'self'"],
11 'frame-ancestors': ["'self'"],
12 'frame-src': [
13 "'self'",
14 // "https://*.stripe.com",
15 // "https://*.facebook.com",
16 // "https://*.facebook.net",
17 // 'https://hcaptcha.com',
18 // 'https://*.hcaptcha.com',
19 ],
20 'manifest-src': ["'self'"],
21 'media-src': ["'self'", 'data:'],
22 'object-src': ["'none'"],
23 'style-src': ["'self'", "'unsafe-inline'"],
24 // 'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'],
25 'default-src': [
26 'self',
27 ...(rootDomain ? [rootDomain, `ws://${rootDomain}`] : []),
28 // 'https://*.google.com',
29 // 'https://*.googleapis.com',
30 // 'https://*.firebase.com',
31 // 'https://*.gstatic.com',
32 // 'https://*.cloudfunctions.net',
33 // 'https://*.algolia.net',
34 // 'https://*.facebook.com',
35 // 'https://*.facebook.net',
36 // 'https://*.stripe.com',
37 // 'https://*.sentry.io',
38 ],
39 'script-src': [
40 'self',
41 // 'https://*.stripe.com',
42 // 'https://*.facebook.com',
43 // 'https://*.facebook.net',
44 // 'https://hcaptcha.com',
45 // 'https://*.hcaptcha.com',
46 // 'https://*.sentry.io',
47 // 'https://polyfill.io',
48 ],
49 'worker-src': ["'self'"],
50 // remove report-to & report-uri if you do not want to use Sentry reporting
51 'report-to': ["'csp-endpoint'"],
52 'report-uri': [
53 `https://sentry.io/api/${process.env.VITE_SENTRY_PROJECT_ID}/security/?sentry_key=${process.env.VITE_SENTRY_KEY}`,
54 ],
55 };
56
57 export default cspDirectives;

🎬 First Attempt #

You will need to build the site to see its magic work:

    
pnpm build
pnpm preview

Now, if you open up the Inspector in your browser dev tools, then you should be able to find a meta tag which includes the content security policy.

blurry low resolution placeholder image SvelteKit Content Security Policy: Screenshot shows browser dev tools with Inspector open and the Content Security Policy meta tag added by Svelte Kit visible.
Screenshot: SvelteKit Content Security Policy: Dev Tools

This is all good, but when I deployed to Netlify and ran a test using the securityheaders.com  site. I was getting nothing back for CSP. For that reason, I tried an alternative approach. An alternative to including CSP in meta tags is to use HTTP headers. Both are valid, though the HTTP header is a stronger approach in most cases . Additionally, using HTTP headers you can add reporting, using a service like Sentry. This gives you a heads-up if users start getting CSP errors in their browser.

📜 Header Script #

Netlify, as well as Cloudflare Pages, let you specify HTTP headers for your static site by you including a _headers file in your static folder. The hosts parse this before deploy and then remove it (so it will not be served). My idea was to write a node script which we could run after the site is built. That script would crawl the build folder for HTML files and then extract the content security meta tag and add it to a _headers entry for the page.

Here is the node script I wrote. If you want to try a similar approach, hopefully it will not be too much work for you to tweak it to suit your own use case.

generate-headers.js
javascript
    
1 import 'dotenv/config';
2 import fs from 'fs';
3 import path from 'path';
4 import { parse } from 'node-html-parser';
5
6 const __dirname = path.resolve();
7 const buildDir = path.join(__dirname, 'build');
8
9 const { VITE_SENTRY_ORG_ID, VITE_SENTRY_KEY, VITE_SENTRY_PROJECT_ID } = process.env;
10
11 function removeCspMeta(inputFile) {
12 const fileContents = fs.readFileSync(inputFile, { encoding: 'utf-8' });
13 const root = parse(fileContents);
14 const element = root.querySelector('head meta[http-equiv="content-security-policy"]');
15 const content = element.getAttribute('content');
16 root.remove(element);
17 return content;
18 }
19
20 const cspMap = new Map();
21
22 function findCspMeta(startPath, filter = /.html$/) {
23 if (!fs.existsSync(startPath)) {
24 console.error(`Unable to find CSP start path: ${startPath}`);
25 return;
26 }
27 const files = fs.readdirSync(startPath);
28 files.forEach((item) => {
29 const filename = path.join(startPath, item);
30 const stat = fs.lstatSync(filename);
31 if (stat.isDirectory()) {
32 findCspMeta(filename, filter);
33 } else if (filter.test(filename)) {
34 cspMap.set(
35 filename
36 .replace(buildDir, '')
37 .replace(/.html$/, '')
38 .replace(/^/index$/, '/'),
39 removeCspMeta(filename),
40 );
41 }
42 });
43 }
44
45 function createHeaders() {
46 const headers = `/*
47 X-Frame-Options: DENY
48 X-XSS-Protection: 1; mode=block
49 X-Content-Type-Options: nosniff
50 Referrer-Policy: strict-origin-when-cross-origin
51 Permissions-Policy: accelerometer=(), camera=(), document-domain=(), encrypted-media=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()
52 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
53 Report-To: {"group": "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://o${VITE_SENTRY_KEY}.ingest.sentry.io/api/${VITE_SENTRY_ORG_ID}/security/?sentry_key=${VITE_SENTRY_PROJECT_ID}"}]}
54 `;
55 const cspArray = [];
56 cspMap.forEach((csp, pagePath) =>
57 cspArray.push(`${pagePath}
58 Content-Security-Policy: ${csp}`),
59 );
60
61 const headersFile = path.join(buildDir, '_headers');
62 fs.writeFileSync(headersFile, `${headers}${cspArray.join('
63 ')}`);
64 }
65
66 async function main() {
67 findCspMeta(buildDir);
68 createHeaders();
69 }
70
71 main();

In lines 47 – 53 you will see I added some other HTTP headers which securityheaders.com looks for. The findCspMeta function, starting in line 22 is what does the heavy lifting for finding meta in the SvelteKit generated output. We also use the node-html-parser package to parse the DOM efficiently. In lines 34 – 40 we add the CSP content to a map with the page path as the key. Later, we use the map to generate the /build/_headers file. We write _headers directly to build, instead of static since we run this script after the SvelteKit build.

Here is an example of the script output:

build/_headers
plaintext
    
/*
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), document-domain=(), encrypted-media=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Report-To: {"group": "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://XXX.ingest.sentry.io/api/XXX/security/?sentry_key=XXX"}]}
/best-medium-format-camera-for-starting-out
Content-Security-Policy: child-src 'self'; default-src 'self'; frame-src 'self'; worker-src 'self'; connect-src 'self' ws://localhost:*; font-src 'self' data:; img-src 'self' data:; manifest-src 'self'; media-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-KD6K876QaEoRcbVCglIUUkrVfvbkkiOzn+MUAYvIE3I=' 'sha256-zArBwCFLmTaX5PiopOgysXsLgzWtw+D2DfdI+gej1y0='; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-to 'csp-endpoint'
/contact
Content-Security-Policy: child-src 'self'; default-src 'self'; frame-src 'self'; worker-src 'self'; connect-src 'self' ws://localhost:*; font-src 'self' data:; img-src 'self' data:; manifest-src 'self'; media-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-t7R4W+8Ou9kpe3an17uRnyxB95SfUTIMJ/K2z6vu0Io=' 'sha256-zArBwCFLmTaX5PiopOgysXsLgzWtw+D2DfdI+gej1y0='; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-to 'csp-endpoint'
/folding-camera
Content-Security-Policy: child-src 'self'; default-src 'self'; frame-src 'self'; worker-src 'self'; connect-src 'self' ws://localhost:*; font-src 'self' data:; img-src 'self' data:; manifest-src 'self'; media-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-4xx4DsEsRBOVYIl2xwCtDOZ+mGnU01sxNiKHZH57Z6w=' 'sha256-zArBwCFLmTaX5PiopOgysXsLgzWtw+D2DfdI+gej1y0='; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-to 'csp-endpoint'
/
Content-Security-Policy: child-src 'self'; default-src 'self'; frame-src 'self'; worker-src 'self'; connect-src 'self' ws://localhost:*; font-src 'self' data:; img-src 'self' data:; manifest-src 'self'; media-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-mXijveCfKQlG2poJkRRzcdCDdFOlpwhP7utTdY0mOtU=' 'sha256-zArBwCFLmTaX5PiopOgysXsLgzWtw+D2DfdI+gej1y0='; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-to 'csp-endpoint'
/twin-lens-reflex-camera
Content-Security-Policy: child-src 'self'; default-src 'self'; frame-src 'self'; worker-src 'self'; connect-src 'self' ws://localhost:*; font-src 'self' data:; img-src 'self' data:; manifest-src 'self'; media-src 'self' data:; object-src 'none'; script-src 'self' 'sha256-w5p2NquSvorJBfJewyjpg4Lm1Mzs7rALuFMPfF7I/OI=' 'sha256-zArBwCFLmTaX5PiopOgysXsLgzWtw+D2DfdI+gej1y0='; style-src 'self' 'unsafe-inline'; base-uri 'self'; form-action 'self'; report-to 'csp-endpoint'

To run the script, we just update the package.json build script:

package.json
json
    
1 {
2 "name": "sveltekit-blog-mdx",
3 "version": "2.0.0",
4 "scripts": {
5 "dev": "vite dev",
6 "build": "npm run generate:manifest && vite build && npm run generate:headers",
7 "preview": "vite preview",
8 "check": "svelte-check --fail-on-hints",
9 "check:watch": "svelte-check --watch",
10 "lint": "prettier --check --plugin=prettier-plugin-svelte . && eslint --ignore-path .gitignore .",
11 "lint:scss": "stylelint "src/**/*.{css,scss,svelte}"",
12 "format": "prettier --write --plugin=prettier-plugin-svelte .",
13 "generate:headers": "node ./generate-headers.js",
14 "generate:images": "node ./generate-responsive-image-data.js",
15 "generate:manifest": "node ./generate-manifest.js",
16 "generate:sitemap": "node ./generate-sitemap.js",
17 "prettier:check": "prettier --check --plugin=prettier-plugin-svelte .",
18 "prepare": "husky install"
19 },
20 // TRUNCATED...
21 }

🗳 Poll #

Do you use a tool like securityheaders.com to scan your sites?
Voting reveals latest results.

💯 SvelteKit Content Security Policy: Testing it Out #

Redeploying to Netlify and testing with securityheaders.com once more, everything now looks better.

blurry low resolution placeholder image SvelteKit Content Security Policy: Screenshot shows content security policy headers found by Security Headers dot com.
Screenshot: SvelteKit Content Security Policy: SecurityHeaders CSP

One thing you might notice, though, is that the score is capped at A (A+ is the highest rating). This is because, for now, we need to include the unsafe-inline directive for styles (see line 23 of csp-directives.mjs).

blurry low resolution placeholder image SvelteKit Content Security Policy: Screenshot shows warning from Security Headers on use of unsafe-inline in styles content security policy.
Screenshot: SvelteKit Content Security Policy: SecurityHeaders warning

This limitation is mentioned in the SvelteKit CSP pull request . The note there says this will not be needed once Svelte Kit moves to using the Web Animations API.

🙌🏽 SvelteKit Content Security Policy: Wrapup #

In this post, we have taken a peek at this new SvelteKit Content Security Policy feature. In particular, we have touched on:

  • why you might go for CSP hashes instead of nonces;
  • a way to extract SvelteKit’s generated CSP meta for each page; and
  • how you can serve CSP security HTTP headers on your static SvelteKit site.

Let me know if you have different or cleaner ways of achieving the same results. You can drop a comment below or reach out for a chat on Element  as well as Twitter @mention 

You can see the full code for this SvelteKit Content Security Policy post in the Rodney Lab Git Hub repo .

🏁 SvelteKit Content Security Policy: Summary #

Can SvelteKit generate content security policy? #

Content security policy is used to guard against cross-site scripting (XSS) attacks and packet sniffing. By adding a few lines of config to your SvelteKit project, you can have SvelteKit generate content security policy meta tags for you. By default, it will generate SHA256 hashes for prerendered pages and nonces for other content and include this data in a meta tag. That tag is placed in HTML head section for each page.

What is the difference between CSP nonces and hashes? #

Generally, hashes offer a more secure approach. For each script included on the page, we generate a cryptographic hash on the server and transmit this with the page. Then the user browser computes a hash of scripts independently. If the browser hash does not match the server transmitted one, there could have been an attempt by a malicious party to alter the script. The browser will not load it. Hashes work well for static sites, since the hashes only need to be generated once, for all users (as the site is built). For SSR pages, the script can be different for each request. To avoid calculating a hash on each request, we instead generate a random string; a nonce. We include the nonce in script tags and in the page CSP meta. Here the browser checks the two nonce match. For improved security, the server should generate a new random nonce for each request.

Do you need to include unsafe-inline in SvelteKit style-src CSP? #

For the moment, you will need to include `unsafe-inline` for style-src CSP in SvelteKit sites. This is different to including `unsafe-inline` for script-src. That said, dropping `unsafe-inline` for styles is optimal. This is because a malicious party might try to manipulate inline CSS. For example, hiding an important warning using CSS. In future versions of SvelteKit, you will not need to set `unsafe-inline for style-src and in the meantime will probably continue to use CSP for the other benefits it brings.`

🙏🏽 SvelteKit Content Security Policy: Feedback #

If you have found this video useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

blurry low resolution placeholder image ask Rodney X (formerly Twitter) avatar

Rodney

@askRodney

Just wrote a new post on a way you might transform the SvelteKit generated CSP meta tags to HTTP headers served on your static ❤️ SvelteKit site.

Tested on

@Netlify and should work on Cloudflare Pages and others.

Hope you find it useful!#staticsveltehttps://t.co/pl4W7gXilG

— Rodney (@askRodney) June 24, 2022

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram . Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Search Engine Optimization among other topics. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Thanks for reading this post. I hope you found it valuable. Please get in touch with your feedback and suggestions for posts you would like to see. Read more about me …

blurry low resolution placeholder image Rodney from Rodney Lab
TAGS:
SVELTEKIT

Reposts:

Reposts

  • John Sim profile avatar
  • Matt Biilmann profile avatar

Likes:

Likes

  • John Sim profile avatar
  • Kevin Matsunaga profile avatar
  • Matt Biilmann profile avatar
Reposts & likes provided by Mastodon & X via Webmentions.

Related Posts

blurry low resolution placeholder image Svelte Video Blog: Vlog with Mux and SvelteKit

Svelte Video Blog: Vlog with Mux and SvelteKit

plus
sveltekit
<PREVIOUS POST
NEXT POST >
LATEST POST >>

Leave a comment …

Your information will be handled in line with our Privacy Policy .

Comments

  • Olivier Gilles Refalo

    great article ty

    3 years ago
    • Rodney

      Thanks for the feedback Olivier, chuffed you appreciated it 😃
      3 years ago

Ask for more

1 Nov 2022 — Astro Server-Side Rendering: Edge Search Site
3 Oct 2022 — Svelte eCommerce Site: SvelteKit Snipcart Storefront
1 Sept 2022 — Get Started with SvelteKit Headless WordPress

Copyright © 2020 – 2025 Rodney Johnson. All Rights Reserved. Please read important copyright and intellectual property information.

  • Home
  • Profile
  • Plus +
  • Newsletter
  • Contact
  • Links
  • Terms of Use
  • Privacy Policy
We use cookies  to enhance visitors’ experience. Please click the “Options” button to make your choice.  Learn more here.