From f46b8ab784eb9af3c4ffbecebb5702b61a5fa69f Mon Sep 17 00:00:00 2001 From: Nathan Friedly Date: Fri, 9 Sep 2016 12:31:10 -0400 Subject: [PATCH] Add Content Security Policy header, fixed obvious security flaws unsafe-eval is still permitted due to use of underscore templates. Fixing that will be a larger undertaking, Changes: * switched templates to escaping injected content * similarly, switched json output to using jquery.text() instead of .html() * moved google analytics js to external file to allow unsafe-inline js to be disabled * also updated to latest SDK and added .env support to aid testing --- .env.example | 3 + .eslintignore | 2 +- app.js | 19 +++--- config/security.js | 54 ++++++++++++++++- package.json | 2 +- public/js/components/App.js | 1 - public/js/demo.js | 6 +- public/js/vendors/google-analytics.js | 7 +++ views/index.ejs | 84 +++++++++++++-------------- 9 files changed, 117 insertions(+), 61 deletions(-) create mode 100644 .env.example create mode 100644 public/js/vendors/google-analytics.js diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..c990e3f5 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# see http://www.ibm.com/watson/developercloud/doc/getting_started/gs-credentials.shtml +TONE_ANALYZER_USERNAME= +TONE_ANALYZER_PASSWORD= diff --git a/.eslintignore b/.eslintignore index d1e4162b..d2f05549 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ coverage -public/js/vendors/prism.js \ No newline at end of file +public/js/vendors/ diff --git a/app.js b/app.js index d2743219..9875fcf5 100755 --- a/app.js +++ b/app.js @@ -16,20 +16,21 @@ 'use strict'; -var express = require('express'), - app = express(), - watson = require('watson-developer-cloud'); +require('dotenv').load({silent: true}); +var express = require('express'), + app = express(); +var ToneAnalyzerV3 = require('watson-developer-cloud/tone-analyzer/v3'); // Bootstrap application settings require('./config/express')(app); // Create the service wrapper -var toneAnalyzer = watson.tone_analyzer({ - url: 'https://gateway.watsonplatform.net/tone-analyzer/api/', - username: '', - password: '', - version_date: '2016-05-19', - version: 'v3' +var toneAnalyzer = new ToneAnalyzerV3({ + // If unspecified here, the TONE_ANALYZER_USERNAME and TONE_ANALYZER_PASSWORD environment properties will be checked + // After that, the SDK will fall back to the bluemix-provided VCAP_SERVICES environment property + // username: '', + // password: '', + version_date: '2016-05-19' }); app.get('/', function(req, res) { diff --git a/config/security.js b/config/security.js index 09634d3f..1e6bb436 100644 --- a/config/security.js +++ b/config/security.js @@ -25,8 +25,58 @@ var cookieParser = require('cookie-parser'); module.exports = function(app) { app.enable('trust proxy'); - // 1. helmet with defaults - app.use(helmet({ cacheControl: false })); + // 1. helmet with custom CSP header + var cspReportUrl = '/report-csp-violation'; + app.use(helmet({ + cacheControl: false, + contentSecurityPolicy: { + // Specify directives as normal. + directives: { + defaultSrc: ["'self'"], // default value for unspecified directives that end in -src + scriptSrc: [ + "'self'", + "'unsafe-eval'", // underscore.js requires this for templates :( + 'https://cdnjs.cloudflare.com/', + 'https://ajax.googleapis.com/', + 'www.google-analytics.com' + ], // jquery cdn, etc. try to avid "'unsafe-inline'" and "'unsafe-eval'" + styleSrc: ["'self'", "'unsafe-inline'"], // no inline css + imgSrc: ['*', 'data:'], // should be "'self'" and possibly 'data:' for most apps, but vr demo loads random user-supplied image urls, and apparently * doesn't include data: URIs + connectSrc: ["'self'", '*.watsonplatform.net'], // ajax domains + // fontSrc: ["'self'"], // cdn? + objectSrc: [], // embeds (e.g. flash) + // mediaSrc: ["'self'", '*.watsonplatform.net'], // allow watson TTS streams + childSrc: [], // child iframes + frameAncestors: [], // parent iframes + formAction: ["'self'"], // where can forms submit to + pluginTypes: [], // e.g. flash, pdf + // sandbox: ['allow-forms', 'allow-scripts', 'allow-same-origin'], // options: allow-forms allow-same-origin allow-scripts allow-top-navigation + reportUri: cspReportUrl + }, + + // Set to true if you only want browsers to report errors, not block them. + // You may also set this to a function(req, res) in order to decide dynamically + // whether to use reportOnly mode, e.g., to allow for a dynamic kill switch. + reportOnly: false, + + // Set to true if you want to blindly set all headers: Content-Security-Policy, + // X-WebKit-CSP, and X-Content-Security-Policy. + setAllHeaders: false, + + // Set to true if you want to disable CSP on Android where it can be buggy. + disableAndroid: false, + + // Set to false if you want to completely disable any user-agent sniffing. + // This may make the headers less compatible but it will be much faster. + // This defaults to `true`. + browserSniff: true + } + })); + // endpoint to report CSP violations + app.post(cspReportUrl, function(req, res) { + console.log('Content Security Policy Violation:\n', req.body); + res.status(204).send(); // 204 = No Content + }); // 2. rate limiting app.use('/api/', rateLimit({ diff --git a/package.json b/package.json index 64794108..f0091cc7 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "express-rate-limit": "^2.1.0", "express-secure-only": "^0.2.1", "helmet": "^2.1.1", - "watson-developer-cloud": "^1.12.4" + "watson-developer-cloud": "^2.2.0" }, "devDependencies": { "babel-eslint": "^6.0.4", diff --git a/public/js/components/App.js b/public/js/components/App.js index 94e242f9..1ac75cd7 100644 --- a/public/js/components/App.js +++ b/public/js/components/App.js @@ -308,7 +308,6 @@ function App(documentTones, sentences, thresholds, selectedSample) { // eslint-d var map = function(item) { var result = item; result.className = _toneLevel(_selectedFilter, item.tone_categories[_searchIndex(_selectedTone)].tones[_searchIndex(_selectedFilter)].score, 'className_OT'); - result.text = result.text.replace(/\r?\n/g, '
'); return result; }; return _originalSentences.map(map); diff --git a/public/js/demo.js b/public/js/demo.js index 8ba2e91b..4d938cdb 100755 --- a/public/js/demo.js +++ b/public/js/demo.js @@ -315,7 +315,7 @@ function allReady(thresholds, sampleText) { */ function updateJSONSentenceTones() { $sentenceJson.empty(); - $sentenceJson.html(JSON.stringify({'sentences_tone': data.sentences_tone}, null, 2)); + $sentenceJson.text(JSON.stringify({'sentences_tone': data.sentences_tone}, null, 2)); } /** @@ -324,7 +324,7 @@ function allReady(thresholds, sampleText) { */ function updateJSONDocumentTones() { $summaryJsonCode.empty(); - $summaryJsonCode.html(JSON.stringify({'document_tone': data.document_tone}, null, 2)); + $summaryJsonCode.text(JSON.stringify({'document_tone': data.document_tone}, null, 2)); } /** @@ -403,7 +403,7 @@ function allReady(thresholds, sampleText) { message = 'You\'ve sent a lot of requests in a short amount of time. ' + 'As the CPU cores cool off a bit, wait a few seonds before sending more requests.'; } - $errorMessage.html(message); + $errorMessage.text(message); $input.show(); $loading.hide(); $output.hide(); diff --git a/public/js/vendors/google-analytics.js b/public/js/vendors/google-analytics.js new file mode 100644 index 00000000..e876a00e --- /dev/null +++ b/public/js/vendors/google-analytics.js @@ -0,0 +1,7 @@ +(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) +})(window,document,'script','//www.google-analytics.com/analytics.js','ga'); + +ga('create', '<$= ga $>', 'auto'); +ga('send', 'pageview'); diff --git a/views/index.ejs b/views/index.ejs index 1bdcaf06..a22f5799 100755 --- a/views/index.ejs +++ b/views/index.ejs @@ -615,29 +615,29 @@ @@ -708,9 +712,9 @@ <% _.each(items, function(item, key, list) { %> - <%= item.score %> + <%- item.score %> - <%= item.text %> + <%- item.text %> <% }); %> @@ -718,13 +722,13 @@ - +