From 9eae4703c97c50e82d959a3e0859fe1553889b15 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 15 Feb 2022 22:14:03 +0100 Subject: feat: add HTTP security headers I also renamed and changed the format of some environment variables so I can reuse them inside the CSP security header. --- .env.example | 29 ++++++++++-------- next-sitemap.js | 2 +- next.config.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++-- src/utils/config.ts | 4 +-- src/utils/helpers/rss.ts | 6 ++-- 5 files changed, 97 insertions(+), 21 deletions(-) diff --git a/.env.example b/.env.example index 8572b45..3200101 100644 --- a/.env.example +++ b/.env.example @@ -1,19 +1,22 @@ -FRONTEND_URL="https://www.frontend.com" -BACKEND_URL="https://www.backend.com" -GRAPHQL_ENDPOINT="/graphql" +APP_FRONTEND_DOMAIN="www.frontend.com" +APP_BACKEND_DOMAIN="www.backend.com" +APP_PROTOCOL="https" +APP_GRAPHQL_ENDPOINT="/graphql" +APP_AUTHOR_NAME="Your Name" +APP_AUTHOR_EMAIL="your@email.com" +APP_AUTHOR_URL="https://www.yourWebsite.com/" +APP_FEED_DESCRIPTION="What you want..." -AUTHOR_NAME="Your Name" -AUTHOR_EMAIL="your@email.com" -AUTHOR_URL="https://www.yourWebsite.com/" -FEED_DESCRIPTION="What you want..." - -NEXT_PUBLIC_FRONTEND_URL="$FRONTEND_URL" -NEXT_PUBLIC_GRAPHQL_API="$BACKEND_URL$GRAPHQL_ENDPOINT" - -NEXT_PUBLIC_MATOMO_SITE_ID=0 -NEXT_PUBLIC_MATOMO_URL="https://matomoUrl.com" +NEXT_PUBLIC_APP_DOMAIN="$APP_FRONTEND_DOMAIN" +NEXT_PUBLIC_APP_PROTOCOL="$APP_PROTOCOL" +NEXT_PUBLIC_GRAPHQL_API="https://$APP_BACKEND_DOMAIN$APP_GRAPHQL_ENDPOINT" +NEXT_PUBLIC_MATOMO_SITE_ID=1 +NEXT_PUBLIC_MATOMO_DOMAIN="www.analyticsDomain.com" # Use this only in development mode. It prevents "unable to verify the first # certificate" error when using a local domain with mkcert certificate for # backend. #NODE_TLS_REJECT_UNAUTHORIZED=0 + +# Set node environment to not install dev dependencies. +#NODE_ENV=production diff --git a/next-sitemap.js b/next-sitemap.js index 9f7346d..c9cd443 100644 --- a/next-sitemap.js +++ b/next-sitemap.js @@ -1,7 +1,7 @@ /** @type {import('next-sitemap').IConfig} */ module.exports = { - siteUrl: process.env.NEXT_PUBLIC_FRONTEND_URL, + siteUrl: `${process.env.NEXT_PUBLIC_APP_PROTOCOL}://${process.env.NEXT_PUBLIC_APP_DOMAIN}`, generateRobotsTxt: true, changefreq: null, priority: null, diff --git a/next.config.js b/next.config.js index 4a3ca39..f3a3b2c 100644 --- a/next.config.js +++ b/next.config.js @@ -1,12 +1,85 @@ const path = require('path'); -const backendDomain = process.env.BACKEND_URL.split('//')[1]; +const backendDomain = process.env.APP_BACKEND_DOMAIN; +const frontendDomain = process.env.APP_FRONTEND_DOMAIN; +const matomoDomain = process.env.NEXT_PUBLIC_MATOMO_DOMAIN; + +const contentSecurityPolicy = ` + default-src 'self' ${backendDomain}; + child-src: 'self' *.${frontendDomain.replace('www.', '')}; + connect-src 'self' ${backendDomain} api.github.com; + font-src 'self'; + frame-src 'self' ${matomoDomain}; + img-src 'self' ${backendDomain} secure.gravatar.com data:; + media-src 'self' data:; + script-src 'self' ${matomoDomain} 'unsafe-inline'; + style-src 'self' 'unsafe-inline'; +`; + +const contentSecurityPolicyDev = ` + default-src 'self' ${backendDomain}; + child-src: 'self' *.${frontendDomain.replace('www.', '')}; + connect-src 'self' ${backendDomain} api.github.com; + font-src 'self'; + frame-src 'self' ${matomoDomain}; + img-src 'self' ${backendDomain} secure.gravatar.com data:; + media-src 'self' data:; + script-src 'self' ${matomoDomain} 'unsafe-inline' 'unsafe-eval'; + style-src 'self' 'unsafe-inline'; +`; + +const securityHeaders = [ + { + key: 'X-DNS-Prefetch-Control', + value: 'on', + }, + { + key: 'Strict-Transport-Security', + value: 'max-age=31536000; includeSubDomains; preload', + }, + { + key: 'X-XSS-Protection', + value: '1; mode=block', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'Permissions-Policy', + value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()', + }, + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'Referrer-Policy', + value: 'strict-origin-when-cross-origin', + }, + { + key: 'Content-Security-Policy', + value: + process.env.NODE_ENV !== 'development' + ? contentSecurityPolicy.replace(/\s{2,}/g, ' ').trim() + : contentSecurityPolicyDev.replace(/\s{2,}/g, ' ').trim(), + }, +]; /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { scrollRestoration: true, }, + async headers() { + return [ + { + // Apply these headers to all routes in your application. + source: '/:path*', + headers: securityHeaders, + }, + ]; + }, i18n: { locales: ['fr'], defaultLocale: 'fr', @@ -30,7 +103,7 @@ const nextConfig = { path.join(__dirname, 'node_modules'), ], }, - webpack: (config, { dev }) => { + webpack: (config) => { config.module.rules.push( { test: /\.pdf/, diff --git a/src/utils/config.ts b/src/utils/config.ts index 07d42a3..86701fe 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -14,10 +14,10 @@ export const settings = { supported: ['en', 'fr'], }, matomo: { - urlBase: process.env.NEXT_PUBLIC_MATOMO_URL || '', + urlBase: `https://${process.env.NEXT_PUBLIC_MATOMO_DOMAIN}` || '', siteId: process.env.NEXT_PUBLIC_MATOMO_SITE_ID || '0', }, postsPerPage: 10, twitterId: '@ArmandPhilippot', - url: process.env.NEXT_PUBLIC_FRONTEND_URL, + url: `${process.env.NEXT_PUBLIC_APP_PROTOCOL}://${process.env.NEXT_PUBLIC_APP_DOMAIN}`, }; diff --git a/src/utils/helpers/rss.ts b/src/utils/helpers/rss.ts index 8a1c801..e10c7f3 100644 --- a/src/utils/helpers/rss.ts +++ b/src/utils/helpers/rss.ts @@ -15,10 +15,10 @@ const getAllPosts = async (): Promise => { }; export const generateFeed = async () => { - const websiteUrl = process.env.FRONTEND_URL ? process.env.FRONTEND_URL : ''; + const websiteUrl = `${process.env.NEXT_PUBLIC_APP_PROTOCOL}://${process.env.NEXT_PUBLIC_APP_DOMAIN}`; const author = { name: settings.name, - email: process.env.AUTHOR_EMAIL, + email: process.env.APP_AUTHOR_EMAIL, link: websiteUrl, }; const copyright = `${settings.name} CC BY SA ${settings.copyright.startYear} - ${settings.copyright.endYear}`; @@ -27,7 +27,7 @@ export const generateFeed = async () => { const feed = new Feed({ author, copyright, - description: process.env.FEED_DESCRIPTION, + description: process.env.APP_FEED_DESCRIPTION, feedLinks: { json: `${websiteUrl}/feed/json`, atom: `${websiteUrl}/feed/atom`, -- cgit v1.2.3