Skip to content
This repository has been archived by the owner on May 3, 2024. It is now read-only.

Commit

Permalink
feat(csp): add env var to disable csp requirement in dev (#640)
Browse files Browse the repository at this point in the history
* feat(disable-csp): initial commit for disabling csp

* feat(disable-csp): added test for not sending header if csp present

* feat(disable-csp): created test cases for checkCsp function within onModuleLoad

* feat(disable-csp): addressing suggestions, using runTime to validate updated tests

* feat(disable-csp): replaced missingCsp value with undefined

* feat(disable-csp): addressed suggestions

* feat(disable-csp): trying to fix snapshot

* feat(disable-csp): reverted snapshot, passing tests no updates on -- -u

* feat(disable-csp): updated on module load setting

* feat(disable-csp): new way of normalizing and validating the value of env var

* feat(disable-csp): asserting as strings rather than booleans, added || empty string

* feat(disable-csp): added documentation to enviornment variables

* feat(disable-csp): added another test case for validateCspIsPresent

* feat(disable-csp): cleaning up some sudgestions

* feat(disable-csp): quick change to docs

Co-authored-by: Jonny Adshead <JAdshead@users.noreply.github.com>
Co-authored-by: Matthew Mallimo <matthew.mallimo@gmail.com>
  • Loading branch information
3 people committed Jan 19, 2022
1 parent e44fecd commit 7fc5e19
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 7 deletions.
28 changes: 28 additions & 0 deletions __tests__/server/config/env/runTime.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('runTime', () => {
const origEnvVarVals = {};
[
'NODE_ENV',
'ONE_DANGEROUSLY_DISABLE_CSP',
'HTTP_PORT',
'HTTP_METRICS_PORT',
'HOLOCRON_MODULE_MAP_URL',
Expand Down Expand Up @@ -67,6 +68,7 @@ describe('runTime', () => {
console.info = jest.fn();
console.warn = jest.fn();
resetEnvVar('NODE_ENV');
resetEnvVar('ONE_DANGEROUSLY_DISABLE_CSP', 'false');
resetEnvVar('HTTP_ONE_APP_DEV_CDN_PORT');
jest.resetModules();
jest.resetAllMocks();
Expand Down Expand Up @@ -125,6 +127,32 @@ describe('runTime', () => {
});
});

describe('ONE_DANGEROUSLY_DISABLE_CSP', () => {
const disableCspEnv = getEnvVarConfig('ONE_DANGEROUSLY_DISABLE_CSP');

it('throws error if ONE_DANGEROUSLY_DISABLE_CSP is set to true and NODE_ENV is not development', () => {
expect(() => disableCspEnv.validate('true')).toThrowError('If you are trying to bypass CSP requirement, NODE_ENV must also be set to development.');
});

it('warns console if both ONE_DANGEROUSLY_DISABLE_CSP and NODE_ENV are set properly', () => {
process.env.NODE_ENV = 'development';
disableCspEnv.validate('true');
expect(console.warn).toHaveBeenCalledWith('ONE_DANGEROUSLY_DISABLE_CSP is true and NODE_ENV is set to development. Content-Security-Policy header will not be set.');
});

it('does not warn or throw if ONE_DANGEROUSLY_DISABLE_CSP is set to false', () => {
expect(() => disableCspEnv.validate('false')).not.toThrow();
});

it('parses input and returns it in lowercase', () => {
expect(disableCspEnv.normalize('TRUE')).toBe('true');
});

it('has a default value', () => {
expect(disableCspEnv.defaultValue).toBe('false');
});
});

describe('HTTP_PORT', () => {
const httpPort = getEnvVarConfig('HTTP_PORT');

Expand Down
10 changes: 10 additions & 0 deletions __tests__/server/middleware/csp.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('csp', () => {

beforeEach(() => {
jest.resetModules();
process.env.ONE_DANGEROUSLY_DISABLE_CSP = 'false';
});

describe('middleware', () => {
Expand Down Expand Up @@ -62,6 +63,15 @@ describe('csp', () => {
expect(headers).not.toHaveProperty('content-security-policy-report-only');
});

it('does not set csp header if ONE_DANGEROUSLY_DISABLE_CSP is present', () => {
process.env.ONE_DANGEROUSLY_DISABLE_CSP = 'true';
const cspMiddleware = requireCSP().default;
cspMiddleware()(req, res, next);
// eslint-disable-next-line no-underscore-dangle
const headers = res._getHeaders();
expect(headers).not.toHaveProperty('Content-Security-Policy');
});

it('defaults to production csp', () => {
delete process.env.NODE_ENV;
const cspMiddleware = requireCSP().default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ exports[`onModuleLoad throws if the root module provides an incompatible version

exports[`onModuleLoad throws when the one app version is incompatible 1`] = `"some-module@1.0.2 is not compatible with this version of one-app (4.43.0-0), it requires ~4.41.0."`;

exports[`onModuleLoad validates csp added to the root module 1`] = `"Root module must provide a valid content security policy"`;
exports[`onModuleLoad validates csp added to the root module 1`] = `"Root module must provide a valid content security policy."`;
10 changes: 10 additions & 0 deletions __tests__/server/utils/onModuleLoad.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import onModuleLoad, {
setModulesUsingExternals,
getModulesUsingExternals,
CONFIGURATION_KEY,
validateCspIsPresent,
} from '../../../src/server/utils/onModuleLoad';
// This named export exists only on the mock
// eslint-disable-next-line import/named
Expand Down Expand Up @@ -59,11 +60,13 @@ jest.mock('../../../src/server/middleware/sendHtml.js', () => ({

const RootModule = () => <h1>Hello, world</h1>;
const csp = "default: 'none'";
const missingCsp = undefined;
describe('onModuleLoad', () => {
const consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation(() => null);
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => null);

beforeEach(() => {
process.env.ONE_DANGEROUSLY_DISABLE_CSP = 'false';
global.getTenantRootModule = () => RootModule;
jest.resetAllMocks();
getServerStateConfig.mockImplementation(() => ({
Expand Down Expand Up @@ -440,4 +443,11 @@ describe('onModuleLoad', () => {

expect(setConfigureRequestLog).toHaveBeenCalledWith(configureRequestLog);
});
it('Throws error if csp and ONE_DANGEROUSLY_DISABLE_CSP is not set', () => {
expect(() => validateCspIsPresent(missingCsp)).toThrow('Root module must provide a valid content security policy.');
});

it('Does not throw if valid csp is present', () => {
expect(() => validateCspIsPresent(csp)).not.toThrow();
});
});
25 changes: 25 additions & 0 deletions docs/api/server/Environment-Variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ One App can be configured via Environment Variables:
* [`ONE_CLIENT_ROOT_MODULE_NAME`](#one_client_root_module_name) ⚠️
* [`ONE_CONFIG_ENV`](#one_config_env) ⚠️
* [`ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS`](#ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS)
* [`ONE_DANGEROUSLY_DISABLE_CSP`](#ONE_DANGEROUSLY_DISABLE_CSP)
* Server Settings
* [`HOLOCRON_SERVER_MAX_MODULES_RETRY`](#holocron_server_max_modules_retry)
* [`HOLOCRON_SERVER_MAX_SIM_MODULES_FETCH`](#holocron_server_max_sim_modules_fetch)
Expand Down Expand Up @@ -536,6 +537,30 @@ ONE_DANGEROUSLY_ACCEPT_BREAKING_EXTERNALS=undefined
```
## `ONE_DANGEROUSLY_DISABLE_CSP`
**Runs In**
* 🚫 Production
* ✅ Development
If set to `true`, one-app will not throw an error when a valid Content Security Policy (CSP) is not present. This flag is meant to allow any module to be ran as a root module and to allow root modules to bypass the CSP requirement so long as `NODE_ENV` is `development`. When `NODE_ENV` is `development` and `ONE_DANGEROUSLY_DISABLE_CSP` is `true` a CSP header will not be set.
**Shape**
```bash
ONE_DANGEROUSLY_DISABLE_CSP=Boolean
```
**Example**
```bash
ONE_DANGEROUSLY_DISABLE_CSP=true
```
**Default Value**
```bash
ONE_DANGEROUSLY_DISABLE_CSP=false
```
## `ONE_ENABLE_POST_TO_MODULE_ROUTES`
**Runs In**
Expand Down
15 changes: 14 additions & 1 deletion src/server/config/env/runTime.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ const runTime = [
valid: ['development', 'production'],
defaultValue: 'production',
},
{
name: 'ONE_DANGEROUSLY_DISABLE_CSP',
normalize: (input) => input.toLowerCase(),
validate: (input) => {
if (input === 'true' && process.env.NODE_ENV !== 'development') {
throw new Error('If you are trying to bypass CSP requirement, NODE_ENV must also be set to development.');
}
if (input === 'true' && process.env.NODE_ENV === 'development') {
console.warn('ONE_DANGEROUSLY_DISABLE_CSP is true and NODE_ENV is set to development. Content-Security-Policy header will not be set.');
}
},
valid: ['true', 'false'],
defaultValue: 'false',
},
// IPv4 port to bind on
{
name: 'HTTP_PORT',
Expand Down Expand Up @@ -195,7 +209,6 @@ const runTime = [
defaultValue: () => false,
},
];

runTime.forEach(preprocessEnvVar);
export { ip };
export default runTime;
7 changes: 5 additions & 2 deletions src/server/middleware/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const cspCache = {
};

export function updateCSP(csp) {
cspCache.policy = csp;
cspCache.policy = csp || '';
}

export function getCSP() {
Expand All @@ -69,7 +69,10 @@ const csp = () => (req, res, next) => {
}

res.scriptNonce = scriptNonce;
res.setHeader('Content-Security-Policy', updatedPolicy);

if (process.env.ONE_DANGEROUSLY_DISABLE_CSP !== 'true') {
res.setHeader('Content-Security-Policy', updatedPolicy);
}
next();
};

Expand Down
10 changes: 7 additions & 3 deletions src/server/utils/onModuleLoad.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ function validateConfig(configValidators, config) {

export const CONFIGURATION_KEY = 'appConfig';

export function validateCspIsPresent(csp) {
if (!csp && process.env.ONE_DANGEROUSLY_DISABLE_CSP !== 'true') {
throw new Error('Root module must provide a valid content security policy.');
}
}

export default function onModuleLoad({
module,
moduleName,
Expand Down Expand Up @@ -108,9 +114,7 @@ export default function onModuleLoad({
}

if (moduleName === serverStateConfig.rootModuleName) {
if (!csp) {
throw new Error('Root module must provide a valid content security policy');
}
validateCspIsPresent(csp);
clearModulesUsingExternals();
if (provideStateConfig) {
setStateConfig(provideStateConfig);
Expand Down

0 comments on commit 7fc5e19

Please sign in to comment.