Skip to content

Commit

Permalink
CSP reconfiguration (#40)
Browse files Browse the repository at this point in the history
* Make csp config more robust such that its can also be disabled

* Update static build mock path

* Update env sample
  • Loading branch information
peterMuriuki authored Oct 16, 2023
1 parent 0c6fab3 commit 0589a02
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 16 deletions.
4 changes: 3 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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:[email protected]:6379/4

Expand Down
7 changes: 3 additions & 4 deletions src/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
// <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com;" >
contentSecurityPolicy: {
directives: EXPRESS_CONTENT_SECURITY_POLICY_CONFIG,
},
contentSecurityPolicy: cspConfig,
crossOriginEmbedderPolicy: false,
}),
);
Expand Down
49 changes: 49 additions & 0 deletions src/app/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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', () => ({
Expand Down
5 changes: 1 addition & 4 deletions src/configs/__mocks__/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
8 changes: 1 addition & 7 deletions src/configs/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions src/configs/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { EXPRESS_CONTENT_SECURITY_POLICY_CONFIG } from './envs';

export type CspSettings = Record<string, null | Iterable<string>> & {
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 };
};

0 comments on commit 0589a02

Please sign in to comment.