diff --git a/src/cspDirectives.js b/src/cspDirectives.js new file mode 100644 index 0000000..696f8d2 --- /dev/null +++ b/src/cspDirectives.js @@ -0,0 +1,73 @@ +import { + PUBLIC_DOMAIN, + PUBLIC_SENTRY_KEY, + PUBLIC_SENTRY_PROJECT_ID, + PUBLIC_WORKER_URL, +} from '$env/static/public'; + + +const rootDomain = PUBLIC_DOMAIN; // or your server IP for dev + +const directives = { + 'base-uri': ["'self'"], + 'child-src': ["'self'"], + // 'connect-src': ["'self'", 'ws://localhost:*'], + 'connect-src': [ + "'self'", + 'ws://localhost:*', + 'https://hcaptcha.com', + 'https://*.hcaptcha.com', + PUBLIC_WORKER_URL, + ], + 'img-src': ["'self'", 'data:'], + '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', + ], + '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', + ], + 'worker-src': ["'self'"], + // remove report-to & report-uri if you do not want to use Sentry reporting + 'report-to': ["'csp-endpoint'"], + 'report-uri': [ + `https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}`, + ], +}; + +export default directives; diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..557758b --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,21 @@ +import { handleErrorWithSentry, replayIntegration } from "@sentry/sveltekit"; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + dsn: 'https://962a7ed3891a335e112746e5c6c6bf42@o4505828687478784.ingest.us.sentry.io/4506871754326016', + 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 new file mode 100644 index 0000000..f46fef6 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,54 @@ +import { sequence } from "@sveltejs/kit/hooks"; +import { handleErrorWithSentry, sentryHandle } from "@sentry/sveltekit"; +import { cspDirectives } from './cspDirectives.js' +import * as Sentry from '@sentry/sveltekit'; +import { + PUBLIC_SENTRY_KEY, + PUBLIC_SENTRY_PROJECT_ID, +} from '$env/static/public'; + +Sentry.init({ + dsn: 'https://962a7ed3891a335e112746e5c6c6bf42@o4505828687478784.ingest.us.sentry.io/4506871754326016', + tracesSampleRate: 1.0, +}); + +const csp = Object.entries(cspDirectives) + .map(([key, arr]) => key + ' ' + arr.join(' ')) + .join('; '); + +export const cspHandle = async ({ event, resolve }) => { + const response = await resolve(event); + response.headers.set('X-Frame-Options', 'SAMEORIGIN'); + response.headers.set('Referrer-Policy', 'no-referrer'); + response.headers.set( + 'Permissions-Policy', + 'accelerometer=(), autoplay=(), camera=(), document-domain=(), encrypted-media=(), fullscreen=(), gyroscope=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()', + ); + response.headers.set('X-Content-Type-Options', 'nosniff'); + /* Switch from Content-Security-Policy-Report-Only to Content-Security-Policy once you are satisifed policy is what you want + * on switch comment out the Report-Only line + */ + response.headers.set('Content-Security-Policy-Report-Only', csp); + // response.headers.set('Content-Security-Policy', csp); + response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload'); + response.headers.set( + 'Expect-CT', + `max-age=86400, report-uri="https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"`, + ); + response.headers.set( + 'Report-To', + `{group: "csp-endpoint", "max_age": 10886400, "endpoints": [{"url": "https://sentry.io/api/${PUBLIC_SENTRY_PROJECT_ID}/security/?sentry_key=${PUBLIC_SENTRY_KEY}"}]}`, + ); + return response; +} + + +// If you have custom handlers, make sure to place them after `sentryHandle()` in the `sequence` function. +export const 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/vite.config.ts b/vite.config.ts index 678e171..93839a5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import { sentrySvelteKit } from "@sentry/sveltekit"; import { purgeCss } from 'vite-plugin-tailwind-purgecss'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vitest/config'; @@ -8,15 +9,18 @@ export default defineConfig({ host: 'localhost', port: 5174, }, - - plugins: [ - sveltekit(), - purgeCss({ - safelist: { - // any selectors that begin with "hljs-" will not be purged - greedy: [/^hljs-/], - }, - }),], + envPrefix: "PUBLIC_", + plugins: [sentrySvelteKit({ + sourceMapsUploadOptions: { + org: "none-b0c3fadae", + project: "javascript-sveltekit" + } + }), sveltekit(), purgeCss({ + safelist: { + // any selectors that begin with "hljs-" will not be purged + greedy: [/^hljs-/], + }, +})], test: { include: ['src/**/*.{test,spec}.{js,ts}'] }, @@ -28,4 +32,4 @@ export default defineConfig({ $routes: path.resolve(__dirname, 'src', 'routes') } } -}); \ No newline at end of file +});