Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions __tests__/server/community-csp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import config from 'config';
import express from 'express';
import request from 'supertest';
import { server as serverFactory } from 'topcoder-react-utils';

jest.mock('dd-trace', () => ({ init: jest.fn() }));
jest.mock('shared', () => ({}));
jest.mock('topcoder-react-lib', () => ({}));
jest.mock('topcoder-react-utils', () => ({ server: jest.fn() }));
jest.mock('utils/SSR', () => ({}));
jest.mock('reducers', () => ({}));
jest.mock('services/money', () => ({}));
jest.mock('utils/xml2json', () => ({}));
jest.mock('server/routes/cdn', () => jest.fn());
jest.mock('server/routes/recruitCRM', () => jest.fn());
jest.mock('server/routes/mmLeaderboard', () => jest.fn());
jest.mock('server/routes/feeds', () => jest.fn());
jest.mock('server/tc-communities', () => jest.fn());
jest.mock('../../webpack.config', () => jest.fn());

describe('community Content-Security-Policy', () => {
let app;

beforeAll(async () => {
require('server'); // eslint-disable-line global-require
app = express();
await serverFactory.mock.calls[0][1].onExpressJsSetup(app);
app.get('*', (req, res) => res.send('Community page'));
});

test.each([
['veterans.topcoder.com', '/'],
['veterans.topcoder.com', '/learn'],
['community-app.topcoder-dev.com', '/__community__/veterans'],
['community-app.topcoder-dev.com', '/__community__/veterans/learn'],
['tco22.topcoder.com', '/'],
['community-app.topcoder-dev.com', '/__community__/tco22'],
])('allows CDN assets and their static-asset redirects on %s%s', async (host, url) => {
const response = await request(app).get(url).set('Host', host);
const directives = response.headers['content-security-policy'].split(';')
.map(directive => directive.trim().split(/\s+/));

['script-src', 'style-src', 'font-src', 'img-src'].forEach((name) => {
const sources = directives.find(directive => directive[0] === name).slice(1);
expect(sources).toContain("'self'");
expect(sources).toContain(config.CDN.PUBLIC);
expect(sources).toContain(`${config.URL.COMMUNITY_APP}/api/cdn/public/static-assets/`);
expect(sources).not.toContain('*');
expect(sources).not.toContain(config.URL.COMMUNITY_APP);
});

expect(directives).toContainEqual(['object-src', "'none'"]);
expect(directives).toContainEqual(['base-uri', "'self'"]);
});

test('does not add the community policy to unrelated pages', async () => {
const response = await request(app).get('/challenges').set('Host', 'www.topcoder.com');
expect(response.headers['content-security-policy']).toBeUndefined();
});

test('preserves the CMS example frame policy', async () => {
const response = await request(app).get('/examples').set('Host', 'www.topcoder.com');
expect(response.headers['content-security-policy'])
.toBe(`frame-ancestors 'self' ${config.URL.CMS_APP}`);
});
});
14 changes: 12 additions & 2 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,12 @@ async function beforeRender(req, suggestedConfig) {
}

/**
* Custom tuning of ExpressJS server.
* @param {Object} server
* Registers application routes and response headers, including the community CSP
* that permits CDN assets redirected to the application's static-asset endpoint.
* Used by serverFactory through its onExpressJsSetup hook.
* @param {Object} server Express application to configure.
* @returns {Promise<undefined>} Resolves after the middleware is registered.
* @throws {Error} Rejects if Express middleware registration fails.
*/
async function onExpressJsSetup(server) {
/* tc-accounts App was designed for browser environment, and its decodeToken()
Expand All @@ -162,6 +166,8 @@ async function onExpressJsSetup(server) {
* fix. */
global.atob = atob;

const staticAssetSource = `${config.URL.COMMUNITY_APP}/api/cdn/public/static-assets/`;

const checkAuthorizationHeader = (req, res, next) => {
if (req.headers.authorization !== `ApiKey ${config.SERVER_API_KEY}`) {
return res.status(403).end();
Expand All @@ -179,6 +185,7 @@ async function onExpressJsSetup(server) {
"default-src 'self';"
+ " script-src 'report-sample' 'self' 'unsafe-inline' 'unsafe-eval'"
+ ` ${config.CDN.PUBLIC}`
+ ` ${staticAssetSource}`
+ ' http://www.google-analytics.com'
+ ' https://www.google-analytics.com'
+ ' https://43d132d5dbff47c59d9d53ad448f93c2.js.ubembed.com'
Expand All @@ -201,6 +208,7 @@ async function onExpressJsSetup(server) {
+ ' https://www.googletagmanager.com;'
+ " style-src 'report-sample' 'self' 'unsafe-inline'"
+ ` ${config.CDN.PUBLIC}`
+ ` ${staticAssetSource}`
+ ' https://d1of0acg2orgco.cloudfront.net'
+ ' https://d1mwkvp2xbqfs9.cloudfront.net'
+ ' https://d24oibycet9bsb.cloudfront.net'
Expand All @@ -225,6 +233,7 @@ async function onExpressJsSetup(server) {
+ " font-src 'self'"
+ ' data:'
+ ` ${config.CDN.PUBLIC}`
+ ` ${staticAssetSource}`
+ ' https://d1of0acg2orgco.cloudfront.net'
+ ' https://d24oibycet9bsb.cloudfront.net'
+ ' https://d1mwkvp2xbqfs9.cloudfront.net'
Expand All @@ -235,6 +244,7 @@ async function onExpressJsSetup(server) {
+ " img-src 'self'"
+ ' data:'
+ ` ${config.CDN.PUBLIC}`
+ ` ${staticAssetSource}`
+ ' https://cdn.segment.com'
+ ' https://d1of0acg2orgco.cloudfront.net'
+ ' https://d24oibycet9bsb.cloudfront.net'
Expand Down
Loading