diff --git a/.env.sample b/.env.sample
index 11daa60..62a4758 100644
--- a/.env.sample
+++ b/.env.sample
@@ -33,8 +33,10 @@ EXPRESS_MAXIMUM_LOGS_FILE_SIZE=5242880 # 5MB
EXPRESS_MAXIMUM_LOG_FILES_NUMBER=5
EXPRESS_LOGS_FILE_PATH='/home/.express/reveal-express-server.log
-# https://github.com/helmetjs/helmet#reference
+# https://github.com/onaio/express-server/blob/f93e6120c683ca2ada29e0e0fa1c99cf0726f5ec/src/configs/settings.ts#L3C15-L3C15
EXPRESS_CONTENT_SECURITY_POLICY_CONFIG=`{"default-src":["'self'", "smartregister.org", "github.com"]}`
+EXPRESS_CONTENT_SECURITY_POLICY_CONFIG=`{"default-src":["'self'", "smartregister.org", "github.com"], useDefaults:false, reportOnly: true}`
+EXPRESS_CONTENT_SECURITY_POLICY_CONFIG=`false`
EXPRESS_REDIS_STAND_ALONE_URL=redis://username:authpassword@127.0.0.1:6379/4
diff --git a/src/app/index.ts b/src/app/index.ts
index 082f9c3..810b3fc 100644
--- a/src/app/index.ts
+++ b/src/app/index.ts
@@ -36,12 +36,12 @@ import {
EXPRESS_SESSION_SECRET,
EXPRESS_REDIS_STAND_ALONE_URL,
EXPRESS_REDIS_SENTINEL_CONFIG,
- EXPRESS_CONTENT_SECURITY_POLICY_CONFIG,
EXPRESS_RESPONSE_HEADERS,
EXPRESS_OPENSRP_SCOPES,
} from '../configs/envs';
import { SESSION_IS_EXPIRED, TOKEN_NOT_FOUND, TOKEN_REFRESH_FAILED } from '../constants';
import { parseOauthClientData, sessionLogout } from './utils';
+import { readCspOptionsConfig } from '../configs/settings';
const opensrpAuth = new ClientOAuth2({
accessTokenUri: EXPRESS_OPENSRP_ACCESS_TOKEN_URL,
@@ -60,14 +60,13 @@ const app = express();
app.use(compression()); // Compress all routes
// helps mitigate cross-site scripting attacks and other known vulnerabilities
+const cspConfig = readCspOptionsConfig();
app.use(
helmet({
// override default contentSecurityPolicy directive like script-src to include cloudflare cdn and github static content
// might consider turning this off to allow individual front-ends set Content-Security-Policy on meta tags themselves if list grows long
//
- contentSecurityPolicy: {
- directives: EXPRESS_CONTENT_SECURITY_POLICY_CONFIG,
- },
+ contentSecurityPolicy: cspConfig,
crossOriginEmbedderPolicy: false,
}),
);
diff --git a/src/app/tests/index.test.ts b/src/app/tests/index.test.ts
index 4d44a7f..58bf64b 100644
--- a/src/app/tests/index.test.ts
+++ b/src/app/tests/index.test.ts
@@ -4,6 +4,7 @@ import ClientOauth2 from 'client-oauth2';
import request from 'supertest';
import express from 'express';
import Redis from 'ioredis';
+import { resolve } from 'path';
import {
EXPRESS_FRONTEND_OPENSRP_CALLBACK_URL,
EXPRESS_SESSION_LOGIN_URL,
@@ -424,6 +425,54 @@ describe('src/index.ts', () => {
done();
});
+ it('can disable express csp configs', (done) => {
+ jest.resetModules();
+ jest.mock('../../configs/envs', () => ({
+ ...jest.requireActual('../../configs/envs'),
+ EXPRESS_CONTENT_SECURITY_POLICY_CONFIG: 'false',
+ EXPRESS_REACT_BUILD_PATH: resolve(__dirname, '../../configs/__mocks__/build'),
+ }));
+ const { default: app2 } = jest.requireActual('../index');
+ request(app2)
+ .get('/')
+ .expect(200)
+ .expect((res) => {
+ const csp = res.headers['content-security-policy'];
+ expect(csp).toBeUndefined();
+ })
+ .catch((err: Error) => {
+ throw err;
+ })
+ .finally(() => {
+ done();
+ });
+ });
+
+ it('can report csp conflicts instead of failing', (done) => {
+ jest.resetModules();
+ jest.mock('../../configs/envs', () => ({
+ ...jest.requireActual('../../configs/envs'),
+ EXPRESS_CONTENT_SECURITY_POLICY_CONFIG: `{"reportOnly": true, "useDefaults": false, "default-src": ["''self''"]}`,
+ EXPRESS_REACT_BUILD_PATH: resolve(__dirname, '../../configs/__mocks__/build'),
+ }));
+ const { default: app2 } = jest.requireActual('../index');
+ request(app2)
+ .get('/')
+ .expect(200)
+ .expect((res) => {
+ const csp = res.headers['content-security-policy'];
+ const cspOnly = res.headers['content-security-policy-report-only'];
+ expect(csp).toBeUndefined();
+ expect(cspOnly).toEqual(`default-src ''self''`);
+ })
+ .catch((err: Error) => {
+ throw err;
+ })
+ .finally(() => {
+ done();
+ });
+ });
+
it('uses single redis node as session storage', (done) => {
jest.resetModules();
jest.mock('../../configs/envs', () => ({
diff --git a/src/configs/__mocks__/envs.ts b/src/configs/__mocks__/envs.ts
index 5019022..b1ddb75 100644
--- a/src/configs/__mocks__/envs.ts
+++ b/src/configs/__mocks__/envs.ts
@@ -67,10 +67,7 @@ export const EXPRESS_MAXIMUM_LOG_FILES_NUMBER = 5;
export const EXPRESS_LOGS_FILE_PATH = './logs/default-error.log';
export const EXPRESS_COMBINED_LOGS_FILE_PATH = './logs/default-error-and-info.log';
-export const EXPRESS_CONTENT_SECURITY_POLICY_CONFIG = {
- 'default-src': ["'self'"],
- reportUri: 'https://example.com',
-};
+export const EXPRESS_CONTENT_SECURITY_POLICY_CONFIG = `{"default-src":["'self'"],"reportUri":"https://example.com"}`;
export const EXPRESS_RESPONSE_HEADERS = {
'Report-To':
diff --git a/src/configs/envs.ts b/src/configs/envs.ts
index 0e74194..804a0e4 100644
--- a/src/configs/envs.ts
+++ b/src/configs/envs.ts
@@ -69,13 +69,7 @@ export const EXPRESS_LOGS_FILE_PATH = process.env.EXPRESS_LOGS_FILE_PATH || './l
export const EXPRESS_COMBINED_LOGS_FILE_PATH =
process.env.EXPRESS_COMBINED_LOGS_FILE_PATH || './logs/default-error-and-info.log';
-const defaultCsp = JSON.stringify({
- 'default-src': ['none'],
-});
-
-export const EXPRESS_CONTENT_SECURITY_POLICY_CONFIG = JSON.parse(
- process.env.EXPRESS_CONTENT_SECURITY_POLICY_CONFIG || defaultCsp,
-);
+export const { EXPRESS_CONTENT_SECURITY_POLICY_CONFIG } = process.env;
// see https://github.com/luin/ioredis#connect-to-redis
export const { EXPRESS_REDIS_STAND_ALONE_URL } = process.env;
diff --git a/src/configs/settings.ts b/src/configs/settings.ts
new file mode 100644
index 0000000..485b143
--- /dev/null
+++ b/src/configs/settings.ts
@@ -0,0 +1,20 @@
+import { EXPRESS_CONTENT_SECURITY_POLICY_CONFIG } from './envs';
+
+export type CspSettings = Record> & {
+ useDefaults?: boolean;
+ reportOnly?: boolean;
+};
+
+/** parse and return helmets' csp policy from an env string */
+export const readCspOptionsConfig = () => {
+ /** leave default behavior in place as the default, to disable env, dev needs to pass false as the value */
+ if (EXPRESS_CONTENT_SECURITY_POLICY_CONFIG === undefined) {
+ return {};
+ }
+ if (EXPRESS_CONTENT_SECURITY_POLICY_CONFIG === 'false') {
+ return false;
+ }
+ const cspConfig = JSON.parse(EXPRESS_CONTENT_SECURITY_POLICY_CONFIG) as CspSettings;
+ const { useDefaults, reportOnly, ...rest } = cspConfig;
+ return { directives: rest, useDefaults, reportOnly };
+};