From 6c48a8e972fccfadb62186cd5fd34eed9bcb8fed Mon Sep 17 00:00:00 2001 From: linkRace Date: Mon, 7 Aug 2017 00:51:15 -0700 Subject: [PATCH] Add support for CSP nonces both/either style/script src options --- CHANGELOG.md | 9 +++++++++ README.md | 2 ++ index.js | 15 ++++++++++++++- lib/csp.js | 18 ++++++++++++++++++ test/csp.js | 30 ++++++++++++++++++++++++++++++ test/mocks/config/nonce.js | 11 +++++++++++ 6 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 test/mocks/config/nonce.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f9425d..23d5f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +##### v1.5.0 + +* Support for nonce for either style-src, script-src, or both +* Lower case headers for improved performance +* Support for referrer-policy +* Allow CSRF cookie options to be set +* Bugfix: return to suppress promise warning + + ##### v1.4.1 * Bugfix: typo in `nosniff` header diff --git a/README.md b/README.md index fff7128..8de14b6 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ Furthermore, parsers must be registered before lusca. * `[{ "img-src": "'self' http:" }, "block-all-mixed-content"]` * `options.reportOnly` Boolean - Enable report only mode. * `options.reportUri` String - URI where to send the report data +* `options.styleNonce` Boolean - Enable nonce for inline style-src, access from `req.locals.nonce` +* `options.scriptNonce` Boolean - Enable nonce for inline script-src, access from `req.locals.nonce` Enables [Content Security Policy](https://www.owasp.org/index.php/Content_Security_Policy) (CSP) headers. diff --git a/index.js b/index.js index 59b7365..90d4fed 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ │ limitations under the License. │ \*───────────────────────────────────────────────────────────────────────────*/ 'use strict'; +var crypto = require('crypto'); /** @@ -23,11 +24,16 @@ * @param {Object} options The configuration object. */ var lusca = module.exports = function (options) { - var headers = []; + var headers = [], nonce; if (options) { Object.keys(lusca).forEach(function (key) { var config = options[key]; + if (key === "csp" && options[key] && (options[key]['styleNonce'] || options[key]['scriptNonce'])) { + nonce = true; + } + + //if (key === 'csp' && lusca[key](config) && lusca[key](config)[styleNonce]) if (config) { headers.push(lusca[key](config)); @@ -38,6 +44,12 @@ var lusca = module.exports = function (options) { return function lusca(req, res, next) { var chain = next; + if (nonce) { + Object.defineProperty(res.locals, 'nonce', { + value: crypto.pseudoRandomBytes(36).toString('base64'), + enumerable: true + }); + } headers.forEach(function (header) { chain = (function (next) { return function (err) { @@ -46,6 +58,7 @@ var lusca = module.exports = function (options) { return; } header(req, res, next); + return; }; }(chain)); }); diff --git a/lib/csp.js b/lib/csp.js index 9b7d2d6..b502bf7 100644 --- a/lib/csp.js +++ b/lib/csp.js @@ -9,6 +9,8 @@ module.exports = function (options) { var policyRules = options && options.policy, isReportOnly = options && options.reportOnly, reportUri = options && options.reportUri, + styleNonce = options && options.styleNonce, + scriptNonce = options && options.scriptNonce, value, name; name = 'content-security-policy'; @@ -27,6 +29,22 @@ module.exports = function (options) { } return function csp(req, res, next) { + if (styleNonce) { + if (value.match(/style-src 'nonce-.{48}'/)) { + value = value.replace(value.match(/'style-src nonce-.{48}'/), 'style-src \'nonce-' + res.locals.nonce + '\''); + } + else { + value = value.replace('style-src', 'style-src \'nonce-' + res.locals.nonce + '\''); + } + } + if (scriptNonce) { + if (value.match(/script-src 'nonce-.{48}'/)) { + value = value.replace(value.match(/script-src 'nonce-.{48}'/)[0], 'script-src \'nonce-' + res.locals.nonce + '\''); + } + else { + value = value.replace('script-src', 'script-src \'nonce-' + res.locals.nonce + '\''); + } + } res.header(name, value); next(); }; diff --git a/test/csp.js b/test/csp.js index be69c18..bd1abe3 100644 --- a/test/csp.js +++ b/test/csp.js @@ -94,4 +94,34 @@ describe('CSP', function () { .expect(200, done); }); }); + + describe('nonce checks', function () { + it('no nonce specified', function (done) { + var config = require('./mocks/config/cspEnforce'), + app = mock({ csp: config }); + + app.get('/', function (req, res) { + res.status(200).end(); + }); + + request(app) + .get('/') + .expect('Content-Security-Policy', /^(?!.*nonce).*$/) + .expect(200, done); + }); + + it('nonce specified', function (done) { + var config = require('./mocks/config/nonce'), + app = mock({ csp: config }); + + app.get('/', function (req, res) { + res.status(200).end(); + }); + + request(app) + .get('/') + .expect('Content-Security-Policy', /nonce/) + .expect(200, done); + }); + }); }); diff --git a/test/mocks/config/nonce.js b/test/mocks/config/nonce.js new file mode 100644 index 0000000..7efaff1 --- /dev/null +++ b/test/mocks/config/nonce.js @@ -0,0 +1,11 @@ +'use strict'; + + +module.exports = { + reportOnly: false, + scriptNonce: true, + policy: { + "default-src": "*", + "script-src": "'unsafe-inline" + } +}; \ No newline at end of file