diff --git a/admin/server/package-lock.json b/admin/server/package-lock.json index f8e5a7730..b56525068 100644 --- a/admin/server/package-lock.json +++ b/admin/server/package-lock.json @@ -8,7 +8,7 @@ "dependencies": { "compression": "^1.7.4", "express": "^4.17.1", - "helmet": "^7.0.0" + "helmet": "^7.1.0" }, "engines": { "node": "20" diff --git a/admin/server/package.json b/admin/server/package.json index 7952d83d9..17587b61e 100644 --- a/admin/server/package.json +++ b/admin/server/package.json @@ -7,7 +7,7 @@ "dependencies": { "compression": "^1.7.4", "express": "^4.17.1", - "helmet": "^7.0.0" + "helmet": "^7.1.0" }, "engines": { "node": "20" diff --git a/admin/server/server.js b/admin/server/server.js index 18e1644ce..bddb48f8f 100644 --- a/admin/server/server.js +++ b/admin/server/server.js @@ -16,24 +16,42 @@ indexFile = indexFile.replace(/\$([A-Z_]+)/g, (match, p1) => { }); app.use(compression()); + +app.disable("x-powered-by"); // Disable the X-Powered-By header as it is not needed and can be used to infer the server technology + app.use( helmet({ contentSecurityPolicy: { directives: { - "script-src": ["'self'", "'unsafe-inline'"], - "img-src": ["'self'", "https:", "data:"], - "default-src": ["'self'", "https:"], - "media-src": ["'self'", "https:"], - "style-src": ["'self'", "https:", "'unsafe-inline'"], - "font-src": ["'self'", "https:", "data:"], + "default-src": ["'none'"], + "script-src-elem": ["'unsafe-inline'", "'self'"], + "style-src-elem": ["'unsafe-inline'", "'self'"], + "style-src-attr": ["'unsafe-inline'"], + "font-src": ["'self'", "data:"], + "connect-src": ["https:"], + "img-src": ["'self'", "data:", "https:"], + "frame-src": ["https:"], + "frame-ancestors": ["'self'"], + upgradeInsecureRequests: process.env.NODE_ENV === "development" ? undefined : [], }, + useDefaults: false, // Avoid default values for not explicitly set directives }, - xXssProtection: false, + xFrameOptions: false, // Disable deprecated header + crossOriginResourcePolicy: "same-origin", // Do not allow cross-origin requests to access the response + crossOriginEmbedderPolicy: false, // value=no-corp + crossOriginOpenerPolicy: true, // value=same-origin strictTransportSecurity: { - maxAge: 63072000, + // Enable HSTS + maxAge: 63072000, // 2 years (recommended when subdomains are included) includeSubDomains: true, preload: true, }, + referrerPolicy: { + policy: "no-referrer", // No referrer information needs to be sent + }, + xContentTypeOptions: true, // value=nosniff + xDnsPrefetchControl: false, // Disable non-standard header as recommended by MDN + xPermittedCrossDomainPolicies: true, // value=none (prevent MIME sniffing) }), ); diff --git a/api/package-lock.json b/api/package-lock.json index 1761cb9de..cb2d246eb 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -35,7 +35,7 @@ "cookie-parser": "^1.4.5", "express": "^4.18.2", "graphql": "^15.5.0", - "helmet": "^4.6.0", + "helmet": "^7.1.0", "nestjs-console": "^8.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^6.0.0", @@ -14620,12 +14620,11 @@ } }, "node_modules/helmet": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz", - "integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==", - "license": "MIT", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", + "integrity": "sha512-g+HZqgfbpXdCkme/Cd/mZkV0aV3BZZZSugecH03kl38m/Kmdx8jKjBikpDj2cr+Iynv4KpYEviojNdTJActJAg==", "engines": { - "node": ">=10.0.0" + "node": ">=16.0.0" } }, "node_modules/hosted-git-info": { diff --git a/api/package.json b/api/package.json index 0c6d2a9e5..34c50abcc 100644 --- a/api/package.json +++ b/api/package.json @@ -77,7 +77,7 @@ "cookie-parser": "^1.4.5", "express": "^4.18.2", "graphql": "^15.5.0", - "helmet": "^4.6.0", + "helmet": "^7.1.0", "nestjs-console": "^8.0.0", "reflect-metadata": "^0.1.13", "rimraf": "^6.0.0", diff --git a/api/project-words.txt b/api/project-words.txt index b954be53f..3b236b022 100644 --- a/api/project-words.txt +++ b/api/project-words.txt @@ -5,4 +5,7 @@ nowait ormconfig pkey schemaname -tablename \ No newline at end of file +tablename +HSTS +nosniff +noopen \ No newline at end of file diff --git a/api/src/app.module.ts b/api/src/app.module.ts index c0737bb0f..8a288cc46 100644 --- a/api/src/app.module.ts +++ b/api/src/app.module.ts @@ -65,7 +65,7 @@ export class AppModule { origin: config.corsAllowedOrigin, methods: ["GET", "POST"], credentials: false, - exposedHeaders: [], + maxAge: 600, }, useGlobalPrefix: true, // See https://docs.nestjs.com/graphql/other-features#execute-enhancers-at-the-field-resolver-level diff --git a/api/src/main.ts b/api/src/main.ts index 4aa1f90a1..14e9f97aa 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -28,11 +28,12 @@ async function bootstrap(): Promise { useContainer(app.select(appModule), { fallbackOnErrors: true }); app.setGlobalPrefix("api"); + app.enableCors({ origin: config.corsAllowedOrigin, methods: ["GET", "POST"], credentials: false, - exposedHeaders: [], + maxAge: 600, }); app.useGlobalInterceptors(new ExceptionInterceptor(config.debug)); @@ -45,9 +46,34 @@ async function bootstrap(): Promise { }), ); + app.disable("x-powered-by"); + app.use( helmet({ - contentSecurityPolicy: false, // configure this when API returns HTML + contentSecurityPolicy: { + directives: { + "default-src": ["'none'"], + }, + useDefaults: false, // Disable default directives + }, + xFrameOptions: false, // Disable non-standard header + strictTransportSecurity: { + // Enable HSTS + maxAge: 63072000, // 2 years (recommended when subdomains are included) + includeSubDomains: true, + preload: true, + }, + referrerPolicy: { + policy: "no-referrer", // No referrer information is sent along with requests + }, + xContentTypeOptions: true, // value="nosniff" (prevent MIME sniffing) + xDnsPrefetchControl: false, // Disable non-standard header + xDownloadOptions: true, // value="noopen" (prevent IE from executing downloads in the context of the site) + xPermittedCrossDomainPolicies: true, // value="none" (prevent the browser from MIME sniffing) + originAgentCluster: true, // value=?1 + crossOriginResourcePolicy: { + policy: "same-site", // This allows the resource to be shared with the same site (all subdomains/ports are included) + }, }), ); app.use(json({ limit: "1mb" })); // increase default limit of 100kb for saving large pages diff --git a/site/next.config.js b/site/next.config.js index 72fced6d4..96731bec9 100644 --- a/site/next.config.js +++ b/site/next.config.js @@ -5,6 +5,31 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", }); +function generateCSP() { + const cspRules = { + "default-src": "'self'", // Needed for SVGs in Firefox (other browsers load SVGs with img-src) + "style-src-elem": "'self' 'unsafe-inline'", + "style-src-attr": "'unsafe-inline'", + "script-src-elem": "'self' 'unsafe-inline'", + "font-src": "data:", + "frame-src": "https://www.youtube-nocookie.com/", + "img-src": `data: 'self' ${process.env.API_URL}`, + "frame-ancestors": process.env.ADMIN_URL, + }; + + // Conditionally add environment-specific rules + if (process.env.NODE_ENV === "development") { + cspRules["script-src"] = "'unsafe-eval'"; // Needed in local development + cspRules["connect-src"] = "ws:"; // Used for hot reloading in local development + } else { + cspRules["upgrade-insecure-requests"] = ""; // Don't use upgrade-insecure-requests with Domain-Setup + } + + return Object.entries(cspRules) + .map(([key, value]) => `${key} ${value}`.trim()) + .join("; "); +} + const cometConfig = require("./src/comet-config.json"); /** @@ -24,50 +49,48 @@ const nextConfig = { experimental: { optimizePackageImports: ["@comet/cms-site"], }, + poweredByHeader: false, // https://nextjs.org/docs/advanced-features/security-headers headers: async () => [ { source: "/:path*", headers: [ { - key: "Strict-Transport-Security", - value: "max-age=63072000; includeSubDomains; preload", + key: "Content-Security-Policy", + value: generateCSP(), }, { - key: "Cross-Origin-Opener-Policy", - value: "same-origin", + key: "Strict-Transport-Security", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + value: "max-age=63072000; includeSubDomains; preload", // 2 years (recommended when subdomains are included) }, { - key: "Permissions-Policy", - value: "", + key: "Cross-Origin-Opener-Policy", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy + value: "same-origin", // Only allow the same origin to open the page in a browsing context }, { - key: "X-Content-Type-Options", - value: "nosniff", + key: "Cross-Origin-Embedder-Policy", + // This value should be set to 'require-corp' as soon as iframe credentialless is supported by all browsers + // https://developer.mozilla.org/en-US/docs/Web/Security/IFrame_credentialless + // https://caniuse.com/mdn-html_elements_iframe_credentialless + value: "unsafe-none", }, { - key: "Referrer-Policy", - value: "strict-origin-when-cross-origin", + key: "Cross-Origin-Resource-Policy", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy + value: "same-site", // Do not allow cross-origin requests to access the response }, { - key: "Content-Security-Policy", - value: ` - default-src 'self'; - form-action 'self'; - object-src 'none'; - img-src 'self' https: data:${process.env.NODE_ENV === "development" ? " http:" : ""}; - media-src 'self' https: data:${process.env.NODE_ENV === "development" ? " http:" : ""}; - style-src 'self' 'unsafe-inline'; - font-src 'self' https: data:; - script-src 'self' 'unsafe-inline' https:${process.env.NODE_ENV === "development" ? " 'unsafe-eval'" : ""}; - connect-src 'self' https:${process.env.NODE_ENV === "development" ? " http:" : ""}; - frame-ancestors ${process.env.ADMIN_URL}; - upgrade-insecure-requests; - block-all-mixed-content; - frame-src 'self' https://*.youtube.com https://*.youtube-nocookie.com; - ` - .replace(/\s{2,}/g, " ") - .trim(), + // + key: "Permissions-Policy", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy + value: "", + }, + { + key: "X-Content-Type-Options", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + value: "nosniff", // Prevent MIME sniffing + }, + { + // This should be changed when using web analytics tools. For example, use "strict-origin-when-cross-origin" for Google Analytics + key: "Referrer-Policy", // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + value: "same-origin", // Only use referer on own domain. }, ...(process.env.ADMIN_URL ? [