@@ -9,13 +9,67 @@ import type { ReqValidateSSOResponse } from './types/auth';
99import { computeRolesFromCookies , computeRolesFromJWT } from './utilities/auth' ;
1010import { reqGatewayForwardCookies } from './utilities/requests' ;
1111
12+ /**
13+ * Build Content Security Policy directives.
14+ * CSP helps prevent XSS attacks by restricting where scripts/resources can be loaded from.
15+ */
16+ function buildCSPDirectives ( ) : string {
17+ // Extract hostnames from URLs for connect-src
18+ const connectSources = [
19+ "'self'" ,
20+ env . PUBLIC_HASURA_CLIENT_URL ,
21+ env . PUBLIC_HASURA_WEB_SOCKET_URL ,
22+ env . PUBLIC_GATEWAY_CLIENT_URL ,
23+ env . PUBLIC_ACTION_CLIENT_URL ,
24+ env . PUBLIC_WORKSPACE_CLIENT_URL ,
25+ ] . filter ( Boolean ) ;
26+
27+ return [
28+ "default-src 'self'" ,
29+ // 'unsafe-inline' needed for Svelte's scoped styles and Monaco editor
30+ // 'unsafe-eval' needed for Monaco editor's syntax highlighting
31+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'" ,
32+ "style-src 'self' 'unsafe-inline'" ,
33+ "img-src 'self' data: blob:" ,
34+ "font-src 'self' data:" ,
35+ // Workers needed for Monaco editor
36+ "worker-src 'self' blob:" ,
37+ `connect-src ${ connectSources . join ( ' ' ) } ` ,
38+ "frame-ancestors 'none'" ,
39+ "form-action 'self'" ,
40+ "base-uri 'self'" ,
41+ "object-src 'none'" ,
42+ ] . join ( '; ' ) ;
43+ }
44+
45+ /**
46+ * Add security headers to response.
47+ * Uses Report-Only mode initially to gather violations without breaking functionality.
48+ * Change to 'Content-Security-Policy' to enforce after testing.
49+ */
50+ function addSecurityHeaders ( response : Response ) : Response {
51+ const csp = buildCSPDirectives ( ) ;
52+
53+ // Use Report-Only mode to monitor violations without blocking
54+ // Change to 'Content-Security-Policy' to enforce after testing
55+ response . headers . set ( 'Content-Security-Policy-Report-Only' , csp ) ;
56+
57+ // Additional security headers
58+ response . headers . set ( 'X-Content-Type-Options' , 'nosniff' ) ;
59+ response . headers . set ( 'X-Frame-Options' , 'DENY' ) ;
60+ response . headers . set ( 'Referrer-Policy' , 'strict-origin-when-cross-origin' ) ;
61+
62+ return response ;
63+ }
64+
1265export const handle : Handle = async ( { event, resolve } ) => {
1366 if ( event . url . pathname . includes ( 'com.chrome.devtools' ) ) {
1467 return new Response ( null , { status : 204 } ) ;
1568 }
1669 if ( event . url . pathname . includes ( 'error' ) || event . url . pathname . includes ( 'oidc' ) ) {
1770 // don't want hooks running on an error page
18- return await resolve ( event ) ;
71+ const response = await resolve ( event ) ;
72+ return addSecurityHeaders ( response ) ;
1973 }
2074 if (
2175 env . PUBLIC_AUTH_OIDC_ENABLED === 'true' &&
@@ -30,17 +84,17 @@ export const handle: Handle = async ({ event, resolve }) => {
3084
3185 try {
3286 if ( env . PUBLIC_AUTH_OIDC_ENABLED === 'true' ) {
33- return await handleOIDCAuth ( { event, resolve } ) ;
87+ return addSecurityHeaders ( await handleOIDCAuth ( { event, resolve } ) ) ;
3488 } else if ( env . PUBLIC_AUTH_SSO_ENABLED === 'true' ) {
35- return await handleSSOAuth ( { event, resolve } ) ;
89+ return addSecurityHeaders ( await handleSSOAuth ( { event, resolve } ) ) ;
3690 } else {
37- return await handleJWTAuth ( { event, resolve } ) ;
91+ return addSecurityHeaders ( await handleJWTAuth ( { event, resolve } ) ) ;
3892 }
3993 } catch ( e ) {
4094 event . locals . user = null ;
4195 }
4296
43- return await resolve ( event ) ;
97+ return addSecurityHeaders ( await resolve ( event ) ) ;
4498} ;
4599
46100/**
0 commit comments