diff --git a/src/cspDirectives.ts b/src/cspDirectives.ts new file mode 100644 index 0000000..f153ab0 --- /dev/null +++ b/src/cspDirectives.ts @@ -0,0 +1,83 @@ +// https://gist.github.com/acoyfellow/d8e86979c66ebea25e1643594e38be73, Rodney Lab + +import { + PUBLIC_DOMAIN, + PUBLIC_SENTRY_KEY, + PUBLIC_SENTRY_PROJECT_ID, + PUBLIC_SENTRY_ORG_ID, + PUBLIC_WORKER_URL +} from '$env/static/public'; + +export const rootDomain = PUBLIC_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_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 index 596fbe4..4f55acc 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,14 +1,14 @@ import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit'; import * as Sentry from '@sentry/sveltekit'; -import { SENTRY_DSN } from '$env/dynamic/private'; -import { NODE_ENV } from '$env/static/private'; +import { + PUBLIC_SENTRY_KEY, + PUBLIC_SENTRY_PROJECT_ID, + PUBLIC_SENTRY_ORG_ID +} from '$env/static/public'; Sentry.init({ - dsn: SENTRY_DSN, - - if (NODE_ENV === 'production') { - tracesSampleRate: 1.0, - } else {}, + 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 diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e6c052d..40663cf 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,12 +1,52 @@ +import type { Handle } from '@sveltejs/kit'; import { sequence } from '@sveltejs/kit/hooks'; + +import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit'; import * as Sentry from '@sentry/sveltekit'; -import { SENTRY_DSN } from '$env/dynamic/private'; +import { + PUBLIC_SENTRY_KEY, + PUBLIC_SENTRY_PROJECT_ID, + PUBLIC_SENTRY_ORG_ID +} from '$env/static/public'; + +import { csp, rootDomain } from './cspDirectives'; Sentry.init({ - dsn: SENTRY_DSN, - tracesSampleRate: 1 + dsn: `https://${PUBLIC_SENTRY_KEY}@${PUBLIC_SENTRY_ORG_ID}.ingest.us.sentry.io/${PUBLIC_SENTRY_PROJECT_ID}`, + tracesSampleRate: 1.0 }); -export const handleError = Sentry.handleErrorWithSentry(); +export const cspHandle: Handle = async ({ event, resolve }) => { + if (!csp) { + throw new Error('csp is undefined'); + } + const response = await resolve(event); -export const handle = sequence(Sentry.sentryHandle()); + // 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; +}; + +// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function. +export const handle: Handle = sequence(sentryHandle(), cspHandle); + +// 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/ diff --git a/src/hooks.ts b/src/hooks.ts deleted file mode 100644 index 1efbb71..0000000 --- a/src/hooks.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; -import { sequence } from '@sveltejs/kit/hooks'; - -const handleCSP: Handle = async ({ event, resolve }) => { - const response = await resolve(event); - // Avoid clickjacking attacks, see https://cheatsheetseries.owasp.org/cheatsheets/Clickjacking_Defense_Cheat_Sheet.html - response.headers.set('Content-Security-Policy', 'frame-ancestors *.mattmor.in *;'); - return response; -}; -export const handle = sequence(handleCSP);