Skip to content

Commit

Permalink
chore(fxa-payments-server): fixes #1923 - add CSP to the payments server
Browse files Browse the repository at this point in the history
Remove payments/content consolidated CSP, fix Prettier-ified files, create separate middleware for payments

Fix prettier things

Add Stripe CSP things to payments server

Add more config vars into proper directives

Add stripe checkout API

Remove unneeded directive

Add tests to csp.test.js

Remove unneeded test no-CSP lines

Remove isCspRequired check in payment server (not content server)

csp.enabled true by default

Update CSP violations to report to content server

Template literal return for getOrigin

Update Stripe url doc
  • Loading branch information
LZoog committed Aug 13, 2019
1 parent 47ff101 commit 3fccfd1
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/fxa-content-server/server/lib/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = function(config) {
const cspMiddleware = helmet.contentSecurityPolicy(config.rules);

return htmlOnly((req, res, next) => {
cspMiddleware(req, res, next);
if (isCspRequired(req)) {
cspMiddleware(req, res, next);
} else {
Expand Down
2 changes: 1 addition & 1 deletion packages/fxa-content-server/server/lib/csp/blocking.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const url = require('url');

function getOrigin(link) {
const parsed = url.parse(link);
return parsed.protocol + '//' + parsed.host;
return `${parsed.protocol}//${parsed.host}`;
}

/**
Expand Down
11 changes: 4 additions & 7 deletions packages/fxa-content-server/tests/server/csp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
const { registerSuite } = intern.getInterface('object');
const assert = intern.getPlugin('chai').assert;
const config = require('../../server/lib/configuration');
const BlockingRules = require('../../server/lib/csp/blocking');
const path = require('path');
const blockingRules = require('../../server/lib/csp/blocking');
const proxyquire = require('proxyquire');

const csp = proxyquire(path.join(process.cwd(), 'server', 'lib', 'csp'), {
Expand Down Expand Up @@ -34,15 +34,12 @@ suite.tests['blockingRules'] = function() {
const CDN_SERVER = 'https://static.accounts.firefox.com';
config.set('static_resource_url', CDN_SERVER);

const blockingRules = BlockingRules(config);
const Sources = blockingRules.Sources;
const { Sources, directives, reportOnly } = blockingRules(config);

// Ensure none of the Sources map to `undefined`G
// Ensure none of the Sources map to `undefined`
assert.notProperty(Sources, 'undefined');

assert.isFalse(blockingRules.reportOnly);

const directives = blockingRules.directives;
assert.isFalse(reportOnly);

const connectSrc = directives.connectSrc;
assert.lengthOf(connectSrc, 7);
Expand Down
58 changes: 57 additions & 1 deletion packages/fxa-payments-server/server/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,41 @@ const conf = convict({
env: 'CLIENT_ADDRESS_DEPTH',
format: Number,
},
csp: {
enabled: {
default: true,
doc: 'Send "Content-Security-Policy" header',
env: 'CSP_ENABLED',
},
/*eslint-disable sorting/sort-object-props*/
reportUri: {
default: 'http://accounts.firefox.com/_/csp-violation',
doc: 'Location of "report-uri" for full, blocking CSP rules',
env: 'CSP_REPORT_URI',
},
reportOnly: {
default: false,
doc:
'DEPRECATED - Only send the "Content-Security-Policy-Report-Only" header',
env: 'CSP_REPORT_ONLY',
},
reportOnlyEnabled: {
default: false,
doc: 'Send "Content-Security-Policy-Report-Only" header',
env: 'CSP_REPORT_ONLY_ENABLED',
},
reportOnlyUri: {
default: 'http://accounts.firefox.com/_/csp-violation-report-only',
doc: 'Location of "report-uri" for report-only CSP rules',
env: 'CSP_REPORT_ONLY_URI',
},
/*eslint-enable sorting/sort-object-props*/
},
env: {
default: 'production',
doc: 'The current node.js environment',
env: 'NODE_ENV',
format: ['development', 'production'],
format: ['development', 'production', 'test'],
},
hstsMaxAge: {
default: 31536000, // a year
Expand Down Expand Up @@ -130,6 +160,14 @@ const conf = convict({
format: 'url',
},
},
profileImages: {
url: {
default: 'http://127.0.0.1:1112',
doc: 'The url of the Firefox Account Profile Image Server',
env: 'FXA_PROFILE_IMAGES_URL',
format: 'url',
},
},
},
staticResources: {
directory: {
Expand Down Expand Up @@ -158,6 +196,24 @@ const conf = convict({
env: 'STRIPE_API_KEY',
format: String,
},
apiUrl: {
default: 'https://api.stripe.com',
doc: 'The Stripe API url',
env: 'STRIPE_API_URL',
format: 'url',
},
hooksUrl: {
default: 'https://hooks.stripe.com',
doc: 'The Stripe hooks url',
env: 'STRIPE_HOOKS_URL',
format: 'url',
},
scriptUrl: {
default: 'https://js.stripe.com',
doc: 'The Stripe script url',
env: 'STRIPE_SCRIPT_URL',
format: 'url',
},
},
});

Expand Down
18 changes: 18 additions & 0 deletions packages/fxa-payments-server/server/lib/csp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Middleware to take care of CSP. CSP headers are not sent unless config
// option 'csp.enabled' is set (default true).

'use strict';
const helmet = require('helmet');
const htmlOnly = require('./html-middleware');

module.exports = function(config) {
const cspMiddleware = helmet.contentSecurityPolicy(config.rules);

return htmlOnly((req, res, next) => {
cspMiddleware(req, res, next);
});
};
92 changes: 92 additions & 0 deletions packages/fxa-payments-server/server/lib/csp.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const config = require('../config');
const blockingRules = require('./csp/blocking');

describe('CSP blocking rules', () => {
// force the CDN to be enabled for tests.
const CDN_SERVER = 'https://static.accounts.firefox.com';
config.set('staticResources.url', CDN_SERVER);
const { Sources, directives, reportOnly } = blockingRules(config);

it('does not have a Sources value equal `undefined`', () => {
expect(Sources).not.toHaveProperty('undefined');
expect(reportOnly).toBeFalsy();
});

it('has correct connectSrc directives', () => {
const { connectSrc } = directives;

expect(connectSrc).toHaveLength(6);
expect(connectSrc).toContain(Sources.SELF);
expect(connectSrc).toContain(Sources.AUTH_SERVER);
expect(connectSrc).toContain(Sources.OAUTH_SERVER);
expect(connectSrc).toContain(Sources.PROFILE_SERVER);
expect(connectSrc).toContain(Sources.PAIRING_SERVER_WEBSOCKET);
expect(connectSrc).toContain(Sources.STRIPE_API_URL);
});

it('has correct defaultSrc directives', () => {
const { defaultSrc } = directives;

expect(defaultSrc).toHaveLength(1);
expect(defaultSrc).toContain(Sources.SELF);
});

it('has correct fontSrc directives', () => {
const { fontSrc } = directives;

expect(fontSrc).toHaveLength(2);
expect(fontSrc).toContain(Sources.SELF);
expect(fontSrc).toContain(CDN_SERVER);
});

it('has correct frameSrc directives', () => {
const { frameSrc } = directives;

expect(frameSrc).toHaveLength(2);
expect(frameSrc).toContain(Sources.STRIPE_SCRIPT_URL);
expect(frameSrc).toContain(Sources.STRIPE_HOOKS_URL);
});

it('has correct imgSrc directives', () => {
const { imgSrc } = directives;

expect(imgSrc).toHaveLength(5);
expect(imgSrc).toContain(Sources.SELF);
expect(imgSrc).toContain(Sources.DATA);
expect(imgSrc).toContain(Sources.GRAVATAR);
expect(imgSrc).toContain(Sources.PROFILE_IMAGES_SERVER);
expect(imgSrc).toContain(CDN_SERVER);
});

it('has correct mediaSrc directives', () => {
const { mediaSrc } = directives;

expect(mediaSrc).toHaveLength(1);
expect(mediaSrc).toContain(Sources.BLOB);
});

it('has correct objectSrc directives', () => {
const { mediaSrc } = directives;

expect(mediaSrc).toHaveLength(1);
expect(mediaSrc).toContain(Sources.BLOB);
});

it('has correct scriptSrc directives', () => {
const { scriptSrc } = directives;

expect(scriptSrc).toHaveLength(3);
expect(scriptSrc).toContain(Sources.SELF);
expect(scriptSrc).toContain(Sources.STRIPE_SCRIPT_URL);
expect(scriptSrc).toContain(CDN_SERVER);
});

it('has correct styleSrc directives', () => {
const { styleSrc } = directives;

expect(styleSrc).toHaveLength(3);
expect(styleSrc).toContain(Sources.SELF);
expect(styleSrc).toContain(Sources.UNSAFE_INLINE);
expect(styleSrc).toContain(CDN_SERVER);
});
});
106 changes: 106 additions & 0 deletions packages/fxa-payments-server/server/lib/csp/blocking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// Middleware to take care of CSP. CSP headers are not sent unless config
// option 'csp.enabled' is set (default true).

const url = require('url');

function getOrigin(link) {
const parsed = url.parse(link);
return `${parsed.protocol}//${parsed.host}`;
}

/**
* blockingCspMiddleware is where to declare rules that will cause a resource
* to be blocked if it runs afowl of a rule.
*/
module.exports = function(config) {
const AUTH_SERVER = getOrigin(config.get('servers.auth.url'));
const BLOB = 'blob:';
const CDN_URL = config.get('staticResources.url');
const DATA = 'data:';
const GRAVATAR = 'https://secure.gravatar.com';
const OAUTH_SERVER = getOrigin(config.get('servers.oauth.url'));
const PROFILE_SERVER = getOrigin(config.get('servers.profile.url'));
const PROFILE_IMAGES_SERVER = getOrigin(
config.get('servers.profileImages.url')
);
const PUBLIC_URL = config.get('listen.publicUrl');
const PAIRING_SERVER_WEBSOCKET = PUBLIC_URL.replace(/^http/, 'ws');

const STRIPE_API_URL = getOrigin(config.get('stripe.apiUrl'));
const STRIPE_HOOKS_URL = getOrigin(config.get('stripe.hooksUrl'));
const STRIPE_SCRIPT_URL = getOrigin(config.get('stripe.scriptUrl'));

//
// Double quoted values
//
const NONE = "'none'";
// keyword sources - https://www.w3.org/TR/CSP2/#keyword_source
// Note: "'unsafe-eval'" is not used in this module, and "'unsafe-inline'" is
// needed for Stripe inline styles.
const SELF = "'self'";
const UNSAFE_INLINE = "'unsafe-inline'";

function addCdnRuleIfRequired(target) {
if (CDN_URL !== PUBLIC_URL) {
target.push(CDN_URL);
}
return target;
}

const rules = {
directives: {
connectSrc: [
SELF,
AUTH_SERVER,
OAUTH_SERVER,
PROFILE_SERVER,
PAIRING_SERVER_WEBSOCKET,
STRIPE_API_URL,
],
defaultSrc: [SELF],
fontSrc: addCdnRuleIfRequired([SELF]),
frameSrc: [STRIPE_SCRIPT_URL, STRIPE_HOOKS_URL],
imgSrc: addCdnRuleIfRequired([
SELF,
DATA,
// Gravatar support was removed in #4927, but we don't want
// to break the site for users who already use a Gravatar as
// their profile image.
GRAVATAR,
PROFILE_IMAGES_SERVER,
]),
mediaSrc: [BLOB],
objectSrc: [NONE],
reportUri: config.get('csp.reportUri'),
scriptSrc: addCdnRuleIfRequired([SELF, STRIPE_SCRIPT_URL]),
styleSrc: addCdnRuleIfRequired([SELF, UNSAFE_INLINE]),
},
reportOnly: false,
// Sources are exported for unit tests
Sources: {
//eslint-disable-line sorting/sort-object-props
AUTH_SERVER,
BLOB,
CDN_URL,
DATA,
GRAVATAR,
NONE,
OAUTH_SERVER,
PAIRING_SERVER_WEBSOCKET,
PROFILE_IMAGES_SERVER,
PROFILE_SERVER,
PUBLIC_URL,
STRIPE_API_URL,
STRIPE_HOOKS_URL,
STRIPE_SCRIPT_URL,
SELF,
UNSAFE_INLINE,
},
};

return rules;
};
20 changes: 20 additions & 0 deletions packages/fxa-payments-server/server/lib/csp/report-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/**
* reportOnlyCspMiddleware is where to declare experimental rules that
* will not cause a resource to be blocked if it runs afowl of a rule, but
* will cause the resource to be reported.
*
* If no directives other than `reportUri` are declared, the CSP reportOnly
* middleware will not be added.
*/
module.exports = function(config) {
return {
directives: {
reportUri: config.get('csp.reportOnlyUri'),
},
reportOnly: true,
};
};
Loading

0 comments on commit 3fccfd1

Please sign in to comment.