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 d498823..b5e99d7 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,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 77ed299..5abdfd8 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ │ limitations under the License. │ \*───────────────────────────────────────────────────────────────────────────*/ 'use strict'; +var crypto = require('crypto'); /** @@ -24,10 +25,14 @@ */ var lusca = module.exports = function (options) { var headers = []; + var 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 (config) { headers.push(lusca[key](config)); @@ -38,6 +43,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) { @@ -45,7 +56,7 @@ var lusca = module.exports = function (options) { next(err); return; } - header(req, res, next); + return header(req, res, next); }; }(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