mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 19:42:11 +10:00
Merge pull request #652 from sharetribe/csp-policy
Content Security Policy (CSP)
This commit is contained in:
commit
57d504d18a
10 changed files with 195 additions and 18 deletions
|
|
@ -19,3 +19,4 @@ Documentation for specific topics can be found in the following files:
|
|||
* [Colors and icons](colors-and-icons.md)
|
||||
* [Starting a new customisation project](starting-a-new-customisation-project.md)
|
||||
* [Google Maps](google-maps.md)
|
||||
* [Content Security Policy (CSP)](content-security-policy.md)
|
||||
|
|
|
|||
43
docs/content-security-policy.md
Normal file
43
docs/content-security-policy.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Content Security Policy (CSP)
|
||||
|
||||
According to [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP):
|
||||
|
||||
> Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate
|
||||
> certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These
|
||||
> attacks are used for everything from data theft to site defacement or distribution of malware.
|
||||
|
||||
This document describes how to use a CSP with the Starter App. By default the CSP is disabled. By
|
||||
turning it on, the default whitelist in [server/csp.js](../server/csp.js) works with the all the
|
||||
URLs and tools that come with the application.
|
||||
|
||||
## Setup
|
||||
|
||||
The CSP is configured using the `REACT_APP_CSP` environment variable.
|
||||
|
||||
Possible values:
|
||||
|
||||
* not set: disabled
|
||||
* `REACT_APP_CSP=report`: Enabled, but policy violations are only reported
|
||||
* `REACT_APP_CSP=block`: Enabled. Policy violations are reported and requests that violate the
|
||||
policy are blocked
|
||||
|
||||
If error logging with Sentry is enabled (See [Error logging with Sentry](sentry.md)), the reports
|
||||
are sent to Sentry. Otherwise the reports are just logged in the backend.
|
||||
|
||||
## Extending the policy
|
||||
|
||||
If you want to whitelist new sources (for example adding a new external analytics service) and keep
|
||||
the CSP enabled, you should add the domains to the white list in [server/csp.js](../server/csp.js).
|
||||
|
||||
The easiest way to do this is to first turn on the policy in report mode and then see what policy
|
||||
violations are logged to the browser developer console or to the backend policy violation report
|
||||
URL.
|
||||
|
||||
## Resources
|
||||
|
||||
To understand what CSP is and how browsers work, here are some resources:
|
||||
|
||||
* https://content-security-policy.com/
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
|
||||
* https://ponyfoo.com/articles/content-security-policy-in-express-apps
|
||||
* https://helmetjs.github.io/docs/csp/
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
"array.prototype.find": "^2.0.4",
|
||||
"autosize": "^4.0.0",
|
||||
"basic-auth": "^2.0.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"classnames": "^2.2.5",
|
||||
"compression": "^1.7.1",
|
||||
"cookie-parser": "^1.4.3",
|
||||
|
|
|
|||
|
|
@ -158,5 +158,13 @@
|
|||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<!--!preloadedStateScript-->
|
||||
<!--!script-->
|
||||
|
||||
<!--
|
||||
Note when adding new external scripts/styles/fonts/etc.:
|
||||
If a Content Security Policy (CSP) is turned on, the new URLs
|
||||
should be whitelisted in the policy.
|
||||
|
||||
See docs/content-security-policy.md for more information.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
91
server/csp.js
Normal file
91
server/csp.js
Normal 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,
|
||||
});
|
||||
};
|
||||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -70,5 +70,7 @@ exports.error = (e, code, data) => {
|
|||
Raven.captureException(e, { tags: { code }, extra: data });
|
||||
} else {
|
||||
console.error(e);
|
||||
console.error(code);
|
||||
console.error(data);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ export const TwoNamedLinksWithHeadings = {
|
|||
linksPerRow: 2,
|
||||
links: [
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?1' } },
|
||||
text: 'Link 1',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?2' } },
|
||||
text: 'Link 2',
|
||||
|
|
@ -32,19 +32,19 @@ export const ThreeExternalLinksWithHeadings = {
|
|||
linksPerRow: 3,
|
||||
links: [
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'ExternalLink', href: 'http://example.com/1' },
|
||||
text: 'Link 1',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'ExternalLink', href: 'http://example.com/2' },
|
||||
text: 'Link 2',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'ExternalLink', href: 'http://example.com/3' },
|
||||
text: 'Link 3',
|
||||
|
|
@ -62,25 +62,25 @@ export const FourLinks = {
|
|||
linksPerRow: 2,
|
||||
links: [
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?1' } },
|
||||
text: 'Link 1 with quite a long text that tests how the items below align',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?2' } },
|
||||
text: 'Link 2',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?3' } },
|
||||
text: 'Link 3',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?4' } },
|
||||
text: 'Link 4',
|
||||
|
|
@ -96,38 +96,38 @@ export const SixLinks = {
|
|||
linksPerRow: 3,
|
||||
links: [
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?1' } },
|
||||
text: 'Link 1',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?2' } },
|
||||
searchQuery: '?2',
|
||||
text: 'Link 2',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?3' } },
|
||||
text: 'Link 3',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?4' } },
|
||||
text: 'Link 4',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?5' } },
|
||||
text: 'Link 5',
|
||||
},
|
||||
{
|
||||
imageUrl: 'http://lorempixel.com/648/448/',
|
||||
imageUrl: 'https://lorempixel.com/648/448/',
|
||||
imageAltText,
|
||||
linkProps: { type: 'NamedLink', name: 'SearchPage', to: { search: '?6' } },
|
||||
text: 'Link 6',
|
||||
|
|
|
|||
|
|
@ -62,13 +62,13 @@ export const createImage = id => ({
|
|||
name: 'square',
|
||||
height: 408,
|
||||
width: 408,
|
||||
url: 'https://placehold.it/408x408',
|
||||
url: 'https://via.placeholder.com/408x408',
|
||||
},
|
||||
{
|
||||
name: 'square2x',
|
||||
height: 816,
|
||||
width: 816,
|
||||
url: 'https://placehold.it/816x816',
|
||||
url: 'https://via.placeholder.com/816x816',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1180,7 +1180,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
|||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
|
||||
body-parser@1.18.2:
|
||||
body-parser@1.18.2, body-parser@^1.18.2:
|
||||
version "1.18.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue