From b16c3a04b18fcd89dda8a41e3e6bf48af4612f6e Mon Sep 17 00:00:00 2001 From: matthieu42morin Date: Sun, 28 Apr 2024 14:57:55 +0200 Subject: [PATCH] CSP, sentry, hooks --- src/cspDirectives.ts | 83 ++++++++++++++++++++++++++++++++++++++++++++ src/hooks.client.ts | 22 ++++++++++++ src/hooks.server.ts | 49 +++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/cspDirectives.ts create mode 100644 src/hooks.client.ts diff --git a/src/cspDirectives.ts b/src/cspDirectives.ts new file mode 100644 index 0000000..c425a4c --- /dev/null +++ b/src/cspDirectives.ts @@ -0,0 +1,83 @@ +// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab + +import { + PUBLIC_SITE_DOMAIN, + PUBLIC_SENTRY_KEY, + PUBLIC_SENTRY_PROJECT_ID, + PUBLIC_SENTRY_ORG_ID, + PUBLIC_WORKER_URL +} from '$env/static/public' + +export const rootDomain = PUBLIC_SITE_DOMAIN // or your server IP for dev + +const directives = { + 'base-uri': ["'self'"], + 'child-src': ["'self'", 'blob:'], + // 'connect-src': ["'self'", 'ws://localhost:*'], + 'connect-src': [ + "'self'", + 'ws://localhost:*', + 'https://*.sentry.io', + 'https://hcaptcha.com', + 'https://*.hcaptcha.com', + 'https://*.cartocdn.com', + PUBLIC_SITE_DOMAIN, + PUBLIC_WORKER_URL + ], + 'img-src': ["'self'", 'data:', 'https://images.unsplash.com'], + 'font-src': ["'self'", 'data:'], + 'form-action': ["'self'"], + 'frame-ancestors': ["'self'"], + 'frame-src': [ + "'self'", + // "https://*.stripe.com", + // "https://*.facebook.com", + // "https://*.facebook.net", + 'https://hcaptcha.com', + 'https://*.hcaptcha.com', + 'https://www.openstreetmap.org', + 'https://*.cartocdn.com' + ], + 'manifest-src': ["'self'"], + 'media-src': ["'self'", 'data:'], + 'object-src': ["'none'"], + // 'style-src': ["'self'", "'unsafe-inline'"], + 'style-src': ["'self'", "'unsafe-inline'", 'https://hcaptcha.com', 'https://*.hcaptcha.com'], + 'default-src': [ + "'self'", + rootDomain, + `ws://${rootDomain}`, + // 'https://*.google.com', + // 'https://*.googleapis.com', + // 'https://*.firebase.com', + // 'https://*.gstatic.com', + // 'https://*.cloudfunctions.net', + // 'https://*.algolia.net', + // 'https://*.facebook.com', + // 'https://*.facebook.net', + // 'https://*.stripe.com', + 'https://*.sentry.io' + ], + 'script-src': [ + "'self'", + "'unsafe-inline'", + // 'https://*.stripe.com', + // 'https://*.facebook.com', + // 'https://*.facebook.net', + 'https://hcaptcha.com', + 'https://*.hcaptcha.com', + 'https://*.sentry.io', + // 'https://polyfill.io', + 'https://*.cartocdn.com' + ], + 'worker-src': ["'self'", 'blob:'], + //report-to can throw "Content-Security-Policy: Couldn’t process unknown directive ‘report-to’", leave it for older browsers. + 'report-to': ["'csp-endpoint'"], + 'report-uri': [ + `https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}` + ] +} + +export const csp = Object.entries(directives) + .map(([key, arr]) => key + ' ' + arr.join(' ')) + .join('; ') diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..3281809 --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,22 @@ +import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit' +import * as Sentry from '@sentry/sveltekit' +import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_ORG_ID } from '$env/static/public' + +Sentry.init({ + dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`, + tracesSampleRate: 1.0, + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // If the entire session is not sampled, use the below sample rate to sample + // sessions when an error occurs. + replaysOnErrorSampleRate: 1.0, + + // If you don't want to use Session Replay, just remove the line below: + integrations: [replayIntegration()] +}) + +// If you have a custom error handler, pass it to `handleErrorWithSentry` +export const handleError = handleErrorWithSentry() diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 00cbcc0..250b922 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,7 +1,54 @@ import type { Handle } from '@sveltejs/kit' +import { sequence } from '@sveltejs/kit/hooks' import { site } from '$lib/config/site' -export const handle: Handle = async ({ event, resolve }) => +import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit' +import * as Sentry from '@sentry/sveltekit' +import { PUBLIC_SENTRY_KEY, PUBLIC_SENTRY_PROJECT_ID, PUBLIC_SENTRY_ORG_ID } from '$env/static/public' + +import { csp, rootDomain } from './cspDirectives' + +Sentry.init({ + dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`, + tracesSampleRate: 1.0 +}) + +export const cspHandle: Handle = async ({ event, resolve }) => { + if (!csp) { + throw new Error('csp is undefined') + } + const response = await resolve(event) + + // Permission fullscreen necessary for maps fullscreen + const headers = { + 'X-Frame-Options': 'SAMEORIGIN', + 'Referrer-Policy': 'no-referrer', + 'Permissions-Policy': `accelerometer=(), autoplay=(), camera=(), document-domain=(self, 'js-profiling'), encrypted-media=(), fullscreen=(self ${rootDomain}), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()`, + 'X-Content-Type-Options': 'nosniff', + // 'Content-Security-Policy-Report-Only': csp, + 'Content-Security-Policy': csp, + 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload', + 'Expect-CT': `max-age=86400, report-uri="https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`, + 'Report-To': `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}` + } + + Object.entries(headers).forEach(([key, value]) => { + response.headers.set(key, value) + }) + return response +} + +export const langHandle: Handle = async ({ event, resolve }) => await resolve(event, { transformPageChunk: ({ html }) => html.replace('', ``) }) + +// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function. +export const handle: Handle = sequence(sentryHandle(), cspHandle, langHandle) + +// If you have a custom error handler, pass it to `handleErrorWithSentry` +export const handleError = handleErrorWithSentry() +// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73 +// https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP +// https://scotthelme.co.uk/content-security-policy-an-introduction/ +// scanner: https://securityheaders.com/