Add Content Security Policy (CSP) middleware

This commit is contained in:
Kimmo Puputti 2018-01-17 16:35:42 +02:00
parent 37cd1fdb4c
commit 4552a9cf29
2 changed files with 122 additions and 0 deletions

91
server/csp.js Normal file
View file

@ -0,0 +1,91 @@
const helmet = require('helmet');
const { config } = require('./importer');
const dev = process.env.REACT_APP_ENV === 'development';
const googleAnalyticsEnabled = !!process.env.REACT_APP_GOOGLE_ANALYTICS_ID;
/**
* Middleware for creating a Content Security Policy
*
* @param {String} reportUri URL where the browser will POST the
* policy violation reports
*
* @param {Boolean} enforceSsl When SSL is enforced, all mixed content
* is blocked/reported by the policy
*
* @param {Boolean} reportOnly In the report mode, requests are only
* reported to the report URL instead of blocked
*/
module.exports = (reportUri, enforceSsl, reportOnly) => {
const self = "'self'";
const inline = "'unsafe-inline'";
const sharetribeApi = config.sdk.baseUrl;
const sharetribeAssets = 'https://assets-sharetribecom.sharetribe.com/';
const imgix = '*.imgix.net/';
const loremPixel = 'https://lorempixel.com/';
const placeholder = 'https://via.placeholder.com/';
const googleAnalytics = 'https://www.google-analytics.com/';
const googleMaps = 'https://maps.googleapis.com/';
const googleStaticCsi = 'https://csi.gstatic.com/';
const googleStaticFonts = 'https://fonts.gstatic.com/';
const googleStaticMaps = 'https://maps.gstatic.com/';
const googleFonts = 'https://fonts.googleapis.com/';
const stripeJs = 'https://js.stripe.com/';
const stripeQ = 'https://q.stripe.com/';
const scriptSrc = [self, inline, googleMaps, stripeJs];
const styleSrc = [self, inline, googleFonts];
const imgSrc = [
self,
'data:',
imgix,
loremPixel,
placeholder,
stripeQ,
googleMaps,
googleStaticCsi,
googleStaticMaps,
];
const connectSrc = [self, sharetribeApi, googleMaps];
const fontSrc = [self, sharetribeAssets, googleStaticFonts];
const frameSrc = [self, stripeJs];
if (googleAnalyticsEnabled) {
// Only whitelist Google Analytics URLs when the analytics is
// enabled.
scriptSrc.push(googleAnalytics);
imgSrc.push(googleAnalytics);
}
if (dev) {
// The default Core docker-compose setup server images from this
// URL locally.
imgSrc.push('*.localhost:8000');
}
const directives = {
scriptSrc,
styleSrc,
imgSrc,
connectSrc,
fontSrc,
frameSrc,
formAction: [self],
defaultSrc: [self],
baseUri: [self],
reportUri,
};
if (enforceSsl) {
directives.blockAllMixedContent = true;
}
return helmet.contentSecurityPolicy({
directives,
reportOnly,
});
};

View file

@ -20,6 +20,7 @@ const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const enforceSsl = require('express-enforces-ssl');
const path = require('path');
const sharetribeSdk = require('sharetribe-sdk');
@ -31,6 +32,7 @@ const dataLoader = require('./dataLoader');
const fs = require('fs');
const log = require('./log');
const { sitemapStructure } = require('./sitemap');
const csp = require('./csp');
const buildPath = path.resolve(__dirname, '..', 'build');
const env = process.env.REACT_APP_ENV || 'production';
@ -41,6 +43,9 @@ const CLIENT_ID =
const BASE_URL = process.env.REACT_APP_SHARETRIBE_SDK_BASE_URL || 'http://localhost:8088';
const USING_SSL = process.env.REACT_APP_SHARETRIBE_USING_SSL === 'true';
const TRUST_PROXY = process.env.SERVER_SHARETRIBE_TRUST_PROXY || null;
const CSP = process.env.REACT_APP_CSP;
const cspReportUrl = '/csp-report';
const cspEnabled = CSP === 'block' || CSP === 'report';
const app = express();
const errorPage = fs.readFileSync(path.join(buildPath, '500.html'), 'utf-8');
@ -60,6 +65,23 @@ app.use(log.requestHandler());
// See: https://www.npmjs.com/package/helmet
app.use(helmet());
if (cspEnabled) {
// When a CSP directive is violated, the browser posts a JSON body
// to the defined report URL and we need to parse this body.
app.use(
bodyParser.json({
type: ['json', 'application/csp-report'],
})
);
// CSP can be turned on in report or block mode. In report mode, the
// browser checks the policy and calls the report URL when the
// policy is violated, but doesn't block any requests. In block
// mode, the browser also blocks the requests.
const reportOnly = CSP === 'report';
app.use(csp(cspReportUrl, USING_SSL, reportOnly));
}
// Redirect HTTP to HTTPS if USING_SSL is `true`.
// This also works behind reverse proxies (load balancers) as they are for example used by Heroku.
// In such cases, however, the TRUST_PROXY parameter has to be set (see below)
@ -193,6 +215,15 @@ app.get('*', (req, res) => {
// will be logged there.
app.use(log.errorHandler());
if (cspEnabled) {
// Handler for CSP violation reports.
app.post(cspReportUrl, (req, res) => {
const report = req.body ? req.body['csp-report'] : null;
log.error(new Error('CSP violation'), 'csp-violation', report);
res.status(204).end();
});
}
app.listen(PORT, () => {
const mode = dev ? 'development' : 'production';
console.log(`Listening to port ${PORT} in ${mode} mode`);