added support for csp

This commit is contained in:
Wyatt Johnson
2018-05-17 18:37:50 -06:00
parent 95072be97e
commit c84a9716a1
12 changed files with 120 additions and 19 deletions
+49
View File
@@ -0,0 +1,49 @@
const helmet = require('helmet');
const { WEBSOCKET_LIVE_URI } = require('../config');
const { BASE_PATH, BASE_URL, STATIC_URL } = require('../url');
const { URL } = require('url');
// websocketSrc represents the host where we can connect for websocket requests.
const websocketSrc = new URL(WEBSOCKET_LIVE_URI || BASE_URL).host;
// staticSrc represents any static asset hosted on the static host.
const staticSrc = new URL(STATIC_URL).host;
// nonceSrc represents the nonce source that is used to indicate a safe resource
// to load.
const nonceSrc = (req, res) => `'nonce-${res.locals.nonce}'`;
module.exports = helmet.contentSecurityPolicy({
directives: {
reportUri: `${BASE_PATH}api/v1/csp`, // report all policy violations to our reporting uri
defaultSrc: ["'none'"], // by default, do not allow anything at all
scriptSrc: [
"'self'",
'https://ajax.googleapis.com', // for jquery
staticSrc, // for any static files loaded from a cdn
nonceSrc,
],
styleSrc: [
"'self'",
'https://maxcdn.bootstrapcdn.com', // for bootstrap css
'https://fonts.googleapis.com', // for google fonts
'https://code.getmdl.io', // for mdl css
staticSrc, // for any static files loaded from a cdn
nonceSrc,
],
connectSrc: ["'self'", websocketSrc],
fontSrc: [
"'self'",
'https://maxcdn.bootstrapcdn.com', // for font-awesome
'https://fonts.gstatic.com', // for google fonts
staticSrc, // for any static files loaded from a cdn
nonceSrc,
],
imgSrc: [
"'self'",
staticSrc, // for any static files loaded from a cdn
nonceSrc,
],
},
browserSniff: false,
});
+9
View File
@@ -0,0 +1,9 @@
const uuid = require('uuid/v4');
// nonce is designed to create a random value that can be used in conjunction
// with the csp middleware.
module.exports = (req, res, next) => {
res.locals.nonce = uuid();
next();
};
@@ -2,10 +2,17 @@
{% block title %}{{ t('talk-plugin-notifications.unsubscribe_page.unsubscribe') }}{% endblock %}
{% block css %}
{{ super() }}
<style nonce="{{ nonce }}" type="text/css">
#success { display:none; }
</style>
{% endblock %}
{% block html %}
<div id="root">
<div class="error-console container">{{ t('talk-plugin-notifications.unsubscribe_page.token_invalid') }}</div>
<div id="success" style="display:none;" class="legend container">{{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}</div>
<div id="success" class="legend container">{{ t('talk-plugin-notifications.unsubscribe_page.are_unsubscribed') }}</div>
<form id="unsubscribe-form" class="container">
<legend class="legend">{{ t('talk-plugin-notifications.unsubscribe_page.click_to_confirm') }}</legend>
<button type="submit">{{ t('talk-plugin-notifications.unsubscribe_page.confirm') }}</button>
@@ -15,7 +22,7 @@
{% block js %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
<script nonce="{{ nonce }}" type="text/javascript">
$(function() {
var submitting = false;
var payload = JSON.stringify({token: location.hash.replace('#', '')});
@@ -24,7 +24,7 @@
{% block js %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
<script nonce="{{ nonce }}" type="text/javascript">
$(function() {
function showError(error) {
try {
+33
View File
@@ -0,0 +1,33 @@
const express = require('express');
const Joi = require('joi');
const { logger } = require('../../../services/logging');
const router = express.Router();
const schema = Joi.object().keys({
'csp-report': Joi.object().keys({
'document-uri': Joi.string(),
referrer: Joi.string(),
'blocked-uri': Joi.string(),
'violated-directive': Joi.string(),
'original-policy': Joi.string(),
}),
});
const json = express.json({ type: 'application/csp-report' });
router.post('/', json, async (req, res, next) => {
const { value, error: err } = Joi.validate(req.body, schema, {
stripUnknown: true,
presence: 'required',
});
if (err) {
res.status(400).end();
return;
}
logger.error({ report: value }, 'csp violation reported');
res.status(202).end();
});
module.exports = router;
+1
View File
@@ -9,6 +9,7 @@ router.get('/', (req, res) => {
});
router.use('/account', require('./account'));
router.use('/csp', require('./csp'));
router.use('/assets', require('./assets'));
router.use('/auth', require('./auth'));
router.use('/graph', require('./graph'));
+10 -6
View File
@@ -9,7 +9,9 @@ const path = require('path');
const compression = require('compression');
const plugins = require('../services/plugins');
const staticTemplate = require('../middleware/staticTemplate');
const staticMiddleware = require('express-static-gzip');
const contentSecurityPolicy = require('../middleware/contentSecurityPolicy');
const nonce = require('../middleware/nonce');
const staticServer = require('express-static-gzip');
const { DISABLE_STATIC_SERVER } = require('../config');
const { passport } = require('../services/passport');
const { MOUNT_PATH } = require('../url');
@@ -48,7 +50,7 @@ if (!DISABLE_STATIC_SERVER) {
if (process.env.NODE_ENV === 'production') {
router.use(
'/static',
staticMiddleware(dist, {
staticServer(dist, {
indexFromEmptyFile: false,
enableBrotli: true,
customCompressions: [
@@ -74,10 +76,12 @@ router.use(compression());
// STATIC ROUTES
//==============================================================================
router.use('/admin', staticTemplate, require('./admin'));
router.use('/account', staticTemplate, require('./account'));
router.use('/login', staticTemplate, require('./login'));
router.use('/embed', staticTemplate, require('./embed'));
const staticMiddleware = [staticTemplate, nonce, contentSecurityPolicy];
router.use('/admin', ...staticMiddleware, require('./admin'));
router.use('/account', ...staticMiddleware, require('./account'));
router.use('/login', ...staticMiddleware, require('./login'));
router.use('/embed', ...staticMiddleware, require('./embed'));
//==============================================================================
// PASSPORT MIDDLEWARE
+4 -6
View File
@@ -2,15 +2,13 @@ const express = require('express');
const debug = require('debug')('talk:routes:plugins');
const plugins = require('../services/plugins');
const staticTemplate = require('../middleware/staticTemplate');
const contentSecurityPolicy = require('../middleware/contentSecurityPolicy');
const nonce = require('../middleware/nonce');
const router = express.Router();
// Routes mounted from plugins won't have access to our internal partials
// directory, so we should make that available.
router.use(staticTemplate, (req, res, next) => {
res.locals.root = res.app.get('views');
next();
});
// Apply the middleware.
router.use(staticTemplate, nonce, contentSecurityPolicy);
// Inject server route plugins.
plugins.get('server', 'router').forEach(plugin => {
+1 -1
View File
@@ -17,7 +17,7 @@
{% block js %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script type="text/javascript">
<script nonce="{{ nonce }}" type="text/javascript">
$(function() {
function showError(error) {
try {
+1 -1
View File
@@ -26,7 +26,7 @@
{% block js %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script>
<script nonce="{{ nonce }}" type="text/javascript">
$(function() {
function showError(error) {
try {
+1 -1
View File
@@ -9,6 +9,6 @@
{% endblock %}
{% block js %}
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
<script src="{{ resolve('coral-admin/bundle.js') }}"></script>
{% endblock %}
+1 -1
View File
@@ -11,6 +11,6 @@
{% endblock %}
{% block js %}
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
<script nonce="{{ nonce }}" src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
<script src="{{ resolve('coral-login/bundle.js')}}"></script>
{% endblock %}