diff --git a/README.md b/README.md index 59291c7..6025188 100644 --- a/README.md +++ b/README.md @@ -64,15 +64,18 @@ PROTOCOL= # default: 5000 PORT= - # optional # for MODE: `desktop`, prompts user # for MODE: `server` gets value from api/package.json `configuration.sasPath` SAS_PATH=/path/to/sas/executable.exe -# optional -# When `true` it will disable strict Content Security Policy -CSP_DISABLE=true +# optional HELMET config +# crossOriginEmbedderPolicy flag that will be passed in HELMET config +HELMET_COEP= + +# optional HELMET config +# path to json file that will include HELMET `contentSecurityPolicy` directives +HELMET_CSP_CONFIG_PATH=./csp.config.json # optional # for MODE: `desktop`, prompts user diff --git a/api/.env.example b/api/.env.example index 0a4fecc..577112f 100644 --- a/api/.env.example +++ b/api/.env.example @@ -8,6 +8,9 @@ FULL_CHAIN=fullchain.pem PORT=[5000] default value is 5000 +HELMET_CSP_CONFIG_PATH=./csp.config.json if omitted HELMET default will be used +HELMET_COEP=[true|false] if omitted HELMET default will be used + ACCESS_TOKEN_SECRET= REFRESH_TOKEN_SECRET= AUTH_CODE_SECRET= diff --git a/api/csp.config.example.json b/api/csp.config.example.json new file mode 100644 index 0000000..8ca57a5 --- /dev/null +++ b/api/csp.config.example.json @@ -0,0 +1,5 @@ +{ + "img-src": ["'self'", "domen.com"], + "script-src": ["'self'", "'unsafe-inline'"], + "script-src-attr": ["'self'", "'unsafe-inline'"] +} \ No newline at end of file diff --git a/api/src/app.ts b/api/src/app.ts index f7d9332..262b510 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -17,6 +17,7 @@ import { setProcessVariables, setupFolders } from './utils' +import { getEnvCSPDirectives } from './utils/parseHelmetConfig' dotenv.config() @@ -25,7 +26,8 @@ const app = express() app.use(cookieParser()) app.use(morgan('tiny')) -const { MODE, CORS, WHITELIST, PROTOCOL, CSP_DISABLE } = process.env +const { MODE, CORS, WHITELIST, PROTOCOL, HELMET_CSP_CONFIG_PATH, HELMET_COEP } = + process.env export const cookieOptions = { secure: PROTOCOL === 'https', @@ -33,6 +35,10 @@ export const cookieOptions = { maxAge: 24 * 60 * 60 * 1000 // 24 hours } +const cspConfigJson = getEnvCSPDirectives(HELMET_CSP_CONFIG_PATH) +const coepFlag = + HELMET_COEP === 'true' || HELMET_COEP === undefined ? true : false + /*********************************** * CSRF Protection * ***********************************/ @@ -41,18 +47,17 @@ export const csrfProtection = csrf({ cookie: cookieOptions }) /*********************************** * Handle security and origin * ***********************************/ -if (CSP_DISABLE !== 'true') { - app.use( - helmet({ - contentSecurityPolicy: { - directives: { - ...helmet.contentSecurityPolicy.getDefaultDirectives(), - 'script-src': ["'self'", "'unsafe-inline'"] - } +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + ...helmet.contentSecurityPolicy.getDefaultDirectives(), + ...cspConfigJson } - }) - ) -} + }, + crossOriginEmbedderPolicy: coepFlag + }) +) /*********************************** * Enabling CORS * diff --git a/api/src/utils/parseHelmetConfig.ts b/api/src/utils/parseHelmetConfig.ts new file mode 100644 index 0000000..7310431 --- /dev/null +++ b/api/src/utils/parseHelmetConfig.ts @@ -0,0 +1,33 @@ +import path from 'path' +import fs from 'fs' + +export const getEnvCSPDirectives = ( + HELMET_CSP_CONFIG_PATH: string | undefined +) => { + let cspConfigJson = { + 'script-src': ["'self'", "'unsafe-inline'"] + } + + if ( + typeof HELMET_CSP_CONFIG_PATH === 'string' && + HELMET_CSP_CONFIG_PATH.length > 0 + ) { + const cspConfigPath = path.join(process.cwd(), HELMET_CSP_CONFIG_PATH) + + try { + let file = fs.readFileSync(cspConfigPath).toString() + + try { + cspConfigJson = JSON.parse(file) + } catch (e) { + console.error( + 'Parsing Content Security Policy JSON config failed. Make sure it is valid json' + ) + } + } catch (e) { + console.error('Error reading HELMET CSP config file', e) + } + } + + return cspConfigJson +}