├── .jshintignore ├── tests ├── fixtures │ └── views │ │ ├── test.handlebars │ │ └── layouts │ │ └── main.handlebars └── index.js ├── .npmignore ├── .jshintrc ├── .gitignore ├── package.json ├── .travis.yml ├── LICENSE ├── index.js ├── lib └── csp.js └── README.md /.jshintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /tests/fixtures/views/test.handlebars: -------------------------------------------------------------------------------- 1 | {{cspToken}} 2 | -------------------------------------------------------------------------------- /tests/fixtures/views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | {{{body}}} 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | artifacts/ 3 | **/npm-debug.log 4 | **/ynpm-debug.log 5 | .DS_Store 6 | *~ 7 | test/ 8 | tests/ 9 | docs/ 10 | examples/ 11 | screwdriver/ 12 | Makefile 13 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node" : true, 3 | "curly" : true, 4 | "eqeqeq" : true, 5 | "forin" : true, 6 | "immed" : true, 7 | "newcap" : true, 8 | "noarg" : true, 9 | "noempty" : true, 10 | "undef" : true, 11 | "unused" : "vars", 12 | 13 | "expr" : true, 14 | "validthis": true 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-csp", 3 | "version": "0.1.3", 4 | "description": "Express middleware that simplifies using Content Security Policy", 5 | "licenses": [ 6 | { 7 | "type": "BSD", 8 | "url": "https://github.com/yahoo/express-csp/blob/master/LICENSE" 9 | } 10 | ], 11 | "main": "index.js", 12 | "scripts": { 13 | "pretest": "jshint .", 14 | "test": "istanbul cover -- _mocha tests/" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:yahoo/express-csp.git" 19 | }, 20 | "keywords": [ 21 | "express", 22 | "middleware", 23 | "csp" 24 | ], 25 | "author": "Juan Dopazo ", 26 | "devDependencies": { 27 | "chai": "~1.10.0", 28 | "express": "~4.10.2", 29 | "express-handlebars": "~1.1.0", 30 | "istanbul": "~0.3.6", 31 | "jshint": "^2.6.0", 32 | "mocha": "^2.1.0", 33 | "supertest": "~0.15.0" 34 | }, 35 | "dependencies": { 36 | "lru-cache": "~2.5.0", 37 | "object-assign": "~1.0.0", 38 | "on-headers": "^1.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 0.10 5 | - 0.12 6 | 7 | notifications: 8 | email: 9 | recipients: 10 | - ypt-team@yahoo-inc.com 11 | on_success: change 12 | on_failure: always 13 | 14 | ## 15 | # Ensures that the git tag matches the version in package.json 16 | # 17 | # Example: 18 | # If package.version is `0.0.1` then the git tag must be `0.0.1` 19 | ## 20 | after_success: 21 | - test $(cat $TRAVIS_BUILD_DIR/package.json | grep version | awk '{print $2}' | sed 's/"//g' | sed 's/,//g') = $TRAVIS_TAG && export VALID_VERSION=true 22 | 23 | deploy: 24 | provider: npm 25 | email: ypt-team@yahoo-inc.com 26 | api_key: 27 | secure: qKXWyIEELFeIji1vHkdgPtQjw13zbCDKGr1kAz/Gb8AEMGXfnOZvE7ToM/SNSWsYlXtpE1BqbNBz+4qt33nmCZEoC8jDZEnH33U5mw6qd0v+2BvpC4qA+0fU2KeMvT6/fRxltziT3moYjpFUXLUDb+c4sltOy8EMSnWVy5U9+HjuLWl+2JeWcFGt1O74X4U2bpDWmNS2U2rxuNMR4BmYxLHNwxHnRK8DPcNESB+oa9GELG0sUHC5wbGz4myWqDX8WJWXi97RyUTZJ3/cK+6WjJzig6+K2+Jn6uj6Lhq2juXKC+ZmJMdB12sDbSW5KmJp37I/JZwv0Xuc+r8+f1MJmU0jopHasLq1kd73msEVIdDJekVyQ0RAw2R83spEzN7uDXi56t9zfJs+Iw/FWJTlOnm0mbhoal9H5CmATx9vW+lh5aWD3hsFQYSog9PHwToiS/czqbr6GYYxwYOWvu9ospubL0P6wSUxVDcceYCXsFP9paNgC93kYpIqSSfdQrBblx1gv5Vve6hcBnpajEHK4ud261/ZgedLpv7EYIJyQHMXW2ggYePQQEpyxtHFvSQA79efqgVnq/MQBp6JkdwsfB7E+e1rp15E6CCzTtWSdCjVTlwqOI6qfKEMjRyK2iizl3+NXoyJ/BT+rD7S0wF13HlsfOti91HVDaWi/q2694U= 28 | on: 29 | condition: $VALID_VERSION = true 30 | tags: true 31 | all_branches: true 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Yahoo Inc. All rights reserved. 2 | 3 | Redistribution and use of this software in source and binary forms, 4 | with or without modification, are permitted provided that the following 5 | conditions are met: 6 | 7 | * Redistributions of source code must retain the above 8 | copyright notice, this list of conditions and the 9 | following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the 13 | following disclaimer in the documentation and/or other 14 | materials provided with the distribution. 15 | 16 | * Neither the name of Yahoo Inc. nor the names of its 17 | contributors may be used to endorse or promote products 18 | derived from this software without specific prior 19 | written permission of Yahoo Inc. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 22 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 23 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 24 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved. 3 | * Copyrights licensed under the New BSD License. 4 | * See the accompanying LICENSE file for terms. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var onHeaders = require('on-headers'); 10 | var CSP = require('./lib/csp'); 11 | 12 | function policyBuilder (policy, signedScripts, signedStyles) { 13 | var hasSignedScripts = signedScripts.length > 0; 14 | var hasSignedStyles = signedStyles.length > 0; 15 | var useScriptNonce = !!policy.useScriptNonce; 16 | var useStyleNonce = !!policy.useStyleNonce; 17 | var directives = policy.directives || {}; 18 | var nonce = (useScriptNonce || useStyleNonce) ? '\'nonce-' + this.locals.cspToken + '\' ' : null; 19 | var directiveKeys = Object.keys(directives); 20 | var typePolicy; 21 | 22 | if ((useScriptNonce || hasSignedScripts) && directiveKeys.indexOf('script-src') < 0) { 23 | directiveKeys.push('script-src'); 24 | } 25 | 26 | if ((useStyleNonce || hasSignedStyles) && directiveKeys.indexOf('style-src') < 0) { 27 | directiveKeys.push('style-src'); 28 | } 29 | 30 | return directiveKeys.map(function (type) { 31 | typePolicy = [type].concat(directives[type] || []); 32 | 33 | if ((useScriptNonce && type === 'script-src') || (useStyleNonce && type === 'style-src')) { 34 | typePolicy = typePolicy.concat(nonce); 35 | } 36 | 37 | if (type === 'script-src' && hasSignedScripts) { 38 | typePolicy = typePolicy.concat(signedScripts); 39 | } 40 | else if (type === 'style-src' && hasSignedStyles) { 41 | typePolicy = typePolicy.concat(signedStyles); 42 | } 43 | 44 | return typePolicy.join(' '); 45 | }).join(';'); 46 | } 47 | 48 | exports.extend = function (app, config) { 49 | if (app['@csp']) { return app; } 50 | 51 | Object.defineProperty(app, '@csp', { 52 | value: exports 53 | }); 54 | 55 | var csp = new CSP(config); 56 | 57 | function getSignedStyles (res) { 58 | if (!res._cspSignedStyles) { 59 | Object.defineProperty(res, '_cspSignedStyles', { 60 | value: Object.create(csp.styles) 61 | }); 62 | } 63 | 64 | return res._cspSignedStyles; 65 | } 66 | 67 | function getSignedScripts (res) { 68 | if (!res._cspSignedScripts) { 69 | Object.defineProperty(res, '_cspSignedScripts', { 70 | value: Object.create(csp.scripts) 71 | }); 72 | } 73 | 74 | return res._cspSignedScripts; 75 | } 76 | 77 | app.signScript = function (str) { 78 | csp.signScript(str); 79 | }; 80 | 81 | app.signStyle = function (str) { 82 | csp.signStyle(str); 83 | }; 84 | 85 | app.response.signScript = function (script) { 86 | var scripts = getSignedScripts(this); 87 | scripts[csp.sign(script)] = true; 88 | }; 89 | 90 | app.response.signStyle = function (style) { 91 | var styles = getSignedStyles(this); 92 | styles[csp.sign(style)] = true; 93 | }; 94 | 95 | app.response.setPolicy = function (config) { 96 | try { 97 | Object.defineProperty(this.locals, 'cspPolicies', { 98 | value: csp.parseConfiguration(config), 99 | enumerable: true 100 | }); 101 | } catch (ex) { 102 | throw new Error('The `response.locals.cspPolicies` value must only be set once per request and must always be set through the `setPolicy` method.'); 103 | } 104 | }; 105 | 106 | app.use(function (req, res, next) { 107 | var policy = csp.policies.policy; 108 | var reportPolicy = csp.policies.reportPolicy; 109 | 110 | onHeaders(res, function () { 111 | var scriptKeys = csp.getKeys(getSignedScripts(res)); 112 | var styleKeys = csp.getKeys(getSignedStyles(res)); 113 | var localPolicies = res.locals.cspPolicies; 114 | var policies = localPolicies ? localPolicies.policy : policy; 115 | var reportPolicies = localPolicies ? localPolicies.reportPolicy : reportPolicy; 116 | var policyHeader, reportPolicyHeader; 117 | 118 | if (policies) { 119 | policyHeader = policyBuilder.call(res, policies, scriptKeys, styleKeys); 120 | res.setHeader('Content-Security-Policy', policyHeader); 121 | } 122 | 123 | if (reportPolicies) { 124 | reportPolicyHeader = policyBuilder.call(res, reportPolicies, scriptKeys, styleKeys); 125 | res.setHeader('Content-Security-Policy-Report-Only', reportPolicyHeader); 126 | } 127 | }); 128 | 129 | /** 130 | * Generates a base64 encoded token and stores a nonce token on `res.locals.cspToken`. 131 | */ 132 | if ((policy && (policy.useScriptNonce || policy.useStyleNonce)) || 133 | (reportPolicy && (reportPolicy.useScriptNonce || reportPolicy.useStyleNonce))) { 134 | Object.defineProperty(res.locals, 'cspToken', { 135 | value: csp.createNonceToken(), 136 | enumerable: true 137 | }); 138 | } 139 | 140 | next(); 141 | }); 142 | 143 | return app; 144 | }; 145 | -------------------------------------------------------------------------------- /lib/csp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Yahoo Inc. All rights reserved. 3 | * Copyrights licensed under the New BSD License. 4 | * See the accompanying LICENSE file for terms. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | module.exports = CSP; 10 | 11 | var LRU = require('lru-cache'); 12 | var crypto = require('crypto'); 13 | var TOKEN_RE = new RegExp([ 14 | '(self|none|strict-dynamic|', 15 | 'unsafe-inline|unsafe-eval|unsafe-hashed-attributes)', 16 | '(?!.)|(sha(256|384|512)-|nonce-)' 17 | ].join('')); 18 | 19 | if (!Object.assign) { 20 | Object.assign = require('object-assign'); 21 | } 22 | 23 | var SUPPORTED_DIRECTIVES = [ 24 | 'base-uri', 25 | 'block-all-mixed-content', 26 | 'child-src', 27 | 'connect-src', 28 | 'default-src', 29 | 'font-src', 30 | 'form-action', 31 | 'frame-ancestors', 32 | 'frame-src', 33 | 'img-src', 34 | 'media-src', 35 | 'object-src', 36 | 'plugin-types', 37 | 'report-uri', 38 | 'reflected-xss', 39 | 'require-sri-for', 40 | 'script-src', 41 | 'style-src', 42 | 'upgrade-insecure-requests', 43 | 'worker-src', 44 | 'manifest-src' 45 | ]; 46 | 47 | var freeze = Object.freeze; 48 | 49 | function CSP (config) { 50 | config = Object.assign({}, config, { 51 | cacheSize: 50 52 | }); 53 | 54 | this.scripts = {}; 55 | this.styles = {}; 56 | this.cache = new LRU(config.cacheSize); 57 | this.policies = this.parseConfiguration(config); 58 | } 59 | 60 | CSP.prototype = { 61 | constructor: CSP, 62 | 63 | signScript: function (script) { 64 | this.scripts[this.sign(script)] = true; 65 | }, 66 | 67 | signStyle: function (style) { 68 | this.styles[this.sign(style)] = true; 69 | }, 70 | 71 | sign: function (key) { 72 | var result = this.cache.get(key); 73 | var hash; 74 | 75 | if (!result) { 76 | // The hashing algorithm may be one of: SHA-256, SHA-384 or SHA-512 77 | // See https://w3c.github.io/webappsec/specs/content-security-policy/#source-list-valid-hashes 78 | // Node only supports SHA-256 and SHA-512. Using the 256 version 79 | // because it's faster. 80 | hash = crypto.createHash('sha256'); 81 | hash.update(key, 'utf8'); 82 | // As per the spec in 4.2.5.2.3, this must return the base64 encoded 83 | // version of the digest 84 | result = 'sha256-' + hash.digest('base64'); 85 | this.cache.set(key, result); 86 | } 87 | 88 | return result; 89 | }, 90 | 91 | /** 92 | * Parses an input object for CSP configurations object 93 | * for both `policy`, `reportPolicy` options. 94 | * 95 | * @method parseConfiguration 96 | * @param {Object} CSP Policy Configuration 97 | * @return {Object} Parsed CSP Result (immutable) 98 | */ 99 | parseConfiguration: function (config) { 100 | var policy, reportPolicy; 101 | 102 | if (config.policy) { 103 | policy = freeze({ 104 | useScriptNonce: !!config.policy.useScriptNonce, 105 | useStyleNonce: !!config.policy.useStyleNonce, 106 | directives: config.policy.directives ? this.getDirectives(config.policy.directives) : null 107 | }); 108 | } 109 | 110 | if (config.reportPolicy) { 111 | reportPolicy = freeze({ 112 | useScriptNonce: !!config.reportPolicy.useScriptNonce, 113 | useStyleNonce: !!config.reportPolicy.useStyleNonce, 114 | directives: config.reportPolicy.directives ? this.getDirectives(config.reportPolicy.directives) : null 115 | }); 116 | } 117 | 118 | return freeze({ 119 | policy: policy, 120 | reportPolicy: reportPolicy 121 | }); 122 | }, 123 | 124 | getDirectives: function (config) { 125 | var directives = {}; 126 | 127 | SUPPORTED_DIRECTIVES.forEach(function (directiveName) { 128 | if (Array.isArray(config[directiveName])) { 129 | directives[directiveName] = config[directiveName].filter(function (rule) { 130 | //If policies have been defined, no app level directives can be set 131 | //i.e. this is a request and it is safe to allow the developer to specify 132 | //a nonce 133 | if (this.policies || rule.replace(/\'/, '').indexOf('nonce-') !== 0) { 134 | return true; 135 | } 136 | throw new Error('You cannot explicitly set a nonce at the app level. If you want to use a nonce, set `useScriptNonce` or `useStyleNonce` to true in the config object.'); 137 | }, this) 138 | .map(function (rule) { 139 | if (TOKEN_RE.test(rule)) { 140 | rule = '\'' + rule + '\''; 141 | } 142 | return rule; 143 | }); 144 | freeze(directives[directiveName]); 145 | } 146 | }, this); 147 | 148 | return freeze(directives); 149 | }, 150 | 151 | createNonceToken: function () { 152 | return crypto.pseudoRandomBytes(36).toString('base64'); 153 | }, 154 | 155 | getKeys: function (obj) { 156 | var hashes = []; 157 | var hash; 158 | 159 | // Ignore hasOwnProperty because this function is meant to be called 160 | // on objects created with Object.create(null) 161 | /*jshint forin: false*/ 162 | for (hash in obj) { 163 | hashes.push("'" + hash + "'"); 164 | } 165 | 166 | return hashes; 167 | /*jshint forin: true*/ 168 | } 169 | }; 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | 4 | express-csp 5 | =========== 6 | 7 | [![npm Version][npm-badge]][npm] 8 | [![Build Status][travis-badge]][travis] 9 | 10 | Usage 11 | ----- 12 | 13 | This is an Express extension which allows you to set the [`content-security-policy`](https://w3c.github.io/webappsec/specs/content-security-policy/) for your Express Application. 14 | 15 | API 16 | --- 17 | 18 | ### extend 19 | ```js 20 | var csp = require('express-csp'); 21 | 22 | var app = express(); 23 | 24 | csp.extend(app, { 25 | policy: { 26 | directives: { 27 | 'default-src': ['self', 'https://*.foo.com'], 28 | 'script-src': ['*.apis.bar.com'] 29 | } 30 | }, 31 | reportPolicy: { 32 | useScriptNonce: true, 33 | useStyleNonce: true, 34 | directives: { 35 | 'default-src': ['self', 'https://*.foo.com'], 36 | 'script-src': ['*.apis.bar.com'], 37 | 'plugin-types': ['application/pdf'] 38 | } 39 | } 40 | }); 41 | ``` 42 | 43 | The `extend` method takes two arguments. A reference to the express application, `app`, and 44 | a config object containing the following properties: 45 | 46 | 47 | #### policy 48 | An object containing necessary information to generate policy directives to be added to the [`content-security-policy`](http://w3c.github.io/webappsec/specs/content-security-policy/#content-security-policy-header-field) header. The `policy` object can contain the following possible properties: 49 | 50 | ##### useScriptNonce 51 | 52 | When set to true, a [`nonce`](http://w3c.github.io/webappsec/specs/content-security-policy/#script-src-the-nonce-attribute) will be generated for the `'script-src'` directive of each response and made available as the `res.locals.cspToken` value. This value can then be used in your templates to allow for specified inline script blocks. If [`useStyleNonce`](#useStyleNonce) is also true, the same token will be added to the `'style-src'` directive and the same token will be available for inline style blocks. 53 | 54 | ##### useStyleNonce 55 | 56 | When set to true, a [`nonce`](http://w3c.github.io/webappsec/specs/content-security-policy/#script-src-the-nonce-attribute) will be generated for the `'style-src'` directive of each response and made available as the `res.locals.cspToken` value. This value can then be used in your templates to allow for specified inline script and style blocks. If [`useScriptNonce`](#useScriptNonce) is also true, the same token will be added to the `'script-src'` directive and the same token will be available for inline script blocks. 57 | 58 | ```html 59 | 62 | ``` 63 | 64 | ##### directives 65 | An object of key/value pairs representing [CSP Policy Directives](http://w3c.github.io/webappsec/specs/content-security-policy/#directives) in which the keys refer to the directive 66 | name and the value is an array of rules to apply to that value. 67 | 68 | - [`base-uri`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-base-uri) 69 | - [`block-all-mixed-content`](http://w3c.github.io/webappsec/specs/content-security-policy/#block-all-mixed-content) 70 | - [`child-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-child-src) 71 | - [`connect-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-connect-src) 72 | - [`default-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-default-src) 73 | - [`font-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-font-src) 74 | - [`form-action`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-form-action) 75 | - [`frame-ancestors`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-frame-ancestors) 76 | - [`frame-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#frame-src) 77 | - [`img-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#img-src) 78 | - [`media-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#media-src) 79 | - [`object-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#media-src) 80 | - [`plugin-types`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-plugin-types) 81 | - [`report-uri`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-report-uri) 82 | - [`reflected-xss`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-reflected-xss) 83 | - [`require-sri-for`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-require-sri-for) 84 | - [`script-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-script-src) 85 | - [`style-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-style-src) 86 | - [`upgrade-insecure-requests`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-upgrade-insecure-requests) 87 | - [`worker-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-worker-src) 88 | - [`manifest-src`](http://w3c.github.io/webappsec/specs/content-security-policy/#directive-manifest-src) 89 | 90 | 91 | 92 | #### reportPolicy 93 | An object containing necessary information to generate policy directives to be added to the [`content-security-policy-report-only`](http://w3c.github.io/webappsec/specs/content-security-policy/#content-security-policy-report-only-header-field) header. The `reportPolicy` object can contain the same properties specified for the [`policy`](#policy) object. 94 | 95 | 96 | ### signScript 97 | 98 | Generates and adds a [valid hash](http://w3c.github.io/webappsec/specs/content-security-policy/#source-list-valid-hashes) to the `script-src` directive. 99 | 100 | At the app level 101 | ```js 102 | app.signScript('foo();'); 103 | ``` 104 | 105 | Enables `foo();` throughout the app 106 | ```html 107 | 108 | ``` 109 | At the response level 110 | ```js 111 | app.route('/').get(function (req, res) { 112 | res.signScript('bar();'); 113 | }); 114 | ``` 115 | Enables `bar();` for the route only. 116 | ```html 117 | 118 | ``` 119 | 120 | These will not work with the above examples. 121 | ```html 122 | 125 | 126 | 129 | ``` 130 | 131 | ### signStyle 132 | 133 | Generates and adds a [valid hash](http://w3c.github.io/webappsec/specs/content-security-policy/#source-list-valid-hashes) to the `style-src` directive. 134 | 135 | ```js 136 | app.signStyle('body{background-color:#eee}'); 137 | ``` 138 | 139 | ```js 140 | app.route('/').get(function (req, res) { 141 | res.signStyle('body{background-color:#eee}'); 142 | }); 143 | ``` 144 | 145 | ### res.setPolicy 146 | Allows policy to be set per request. The app level policy set in `extend` will be ignored when `res.setPolicy` is used. This method takes the same config object as the `extend` method. 147 | 148 | ```js 149 | app.get('/', function(req, res, next) { 150 | res.setPolicy({ 151 | policy: { 152 | directives: { 153 | 'script-src' : ['unsafe-inline', '*.foo.com'] 154 | } 155 | }, 156 | reportPolicy: { 157 | useNonce: true, 158 | directives: { 159 | 'script-src' : ['*.foo.com'] 160 | } 161 | } 162 | }); 163 | }); 164 | ``` 165 | ### License 166 | 167 | Code licensed under the BSD license. See [LICENSE file][] file for terms. 168 | 169 | [LICENSE file]: https://github.com/yahoo/express-csp/blob/master/LICENSE 170 | [travis]: https://travis-ci.org/yahoo/express-csp 171 | [travis-badge]: http://img.shields.io/travis/yahoo/express-csp.svg?style=flat-square 172 | [npm]: https://www.npmjs.org/package/express-csp 173 | [npm-badge]: https://img.shields.io/npm/v/express-csp.svg?style=flat-square 174 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, specify*/ 2 | var express = require('express'); 3 | var expect = require('chai').expect; 4 | var request = require('supertest'); 5 | var csp = require('../index'); 6 | var crypto = require('crypto'); 7 | var path = require('path'); 8 | var exphbs = require('express-handlebars'); 9 | 10 | if (!Object.assign) { 11 | Object.assign = require('object-assign'); 12 | } 13 | 14 | var appDefaults = { 15 | policy: { 16 | directives: { 17 | 'script-src': [ 18 | 'https://*.yahoo.com', 19 | 'https://*.syimg.com' 20 | ] 21 | } 22 | } 23 | }; 24 | 25 | var allDirectives = { 26 | 'base-uri' : ['self'], 27 | 'child-src' : ['self', '*.ads.foo.com'], 28 | 'connect-src' : ['self', 'feeds.*.foo.com'], 29 | 'default-src' : ['self' ], 30 | 'font-src' : ['fonts.foo.com'], 31 | 'form-action' : ['self', '*.apis.baz.com'], 32 | 'frame-ancestors' : ['self'], 33 | 'img-src' : ['self', '*.assets.foo.com', '*.images.foo.com'], 34 | 'media-src' : ['self', '*.content.foo.com', '*.videos.bar.com'], 35 | 'object-src' : ['none'], 36 | 'plugin-types' : ['application/pdf'], 37 | 'report-uri' : ['http://www.foo.com/report'], 38 | 'script-src' : ['self', '*.scripts.foo.com', '*.build.baz.com'], 39 | 'style-src' : ['self', 'styles.*.foo.com', '*.styles.bar.com'] 40 | }; 41 | 42 | function createApp(options) { 43 | var app = express(); 44 | csp.extend(app, Object.assign({}, appDefaults, options || {})); 45 | 46 | app.route('/').get(function (req, res) { 47 | res.writeHead(200, { 'Content-Type': 'text/html' }); 48 | res.end('CSP test'); 49 | }); 50 | 51 | return app; 52 | } 53 | 54 | function getCleanPolicies() { 55 | return { 56 | policy: { 57 | directives: { 58 | 'script-src' : ['self', 'unsafe-inline'], 59 | 'style-src' : ['*.foo.com'] 60 | } 61 | }, 62 | reportPolicy: { 63 | directives: { 64 | 'script-src' : ['self', 'unsafe-inline'], 65 | 'style-src' : ['*.foo.com'] 66 | } 67 | } 68 | }; 69 | } 70 | 71 | function testToEnsurePoliciesAreUnchanged(res) { 72 | //get clean copy of the setPolicy object 73 | var testPolicies = getCleanPolicies(); 74 | Object.keys(testPolicies).forEach(function(policyType) { 75 | var cspHeader = policyType === 'reportPolicy' ? 'content-security-policy-report-only' : 'content-security-policy'; 76 | var testPolicy = testPolicies[policyType]; 77 | var testDirectiveKeys = Object.keys(testPolicy.directives); 78 | var policies = res.headers[cspHeader].split(';'); 79 | //there should only be the a 'style-src' and 'script-src' directives 80 | expect(policies.length).to.equal(testDirectiveKeys.length); 81 | policies = policies.map(function(policy) { 82 | return policy.split(' '); 83 | }); 84 | var directiveKeys = policies.map(function(policy) { 85 | return policy[0]; 86 | }); 87 | var directives = {}; 88 | 89 | policies.forEach(function(policy) { 90 | directives[policy.shift()] = policy; 91 | }); 92 | 93 | Object.keys(allDirectives).forEach(function(key) { 94 | if (testDirectiveKeys.indexOf(key) > -1) { 95 | var testDirective = testPolicy.directives[key]; 96 | expect(directiveKeys.indexOf(key)).to.be.above(-1); 97 | testDirective.forEach(function(rule, dirIndex) { 98 | expect(directives[key][dirIndex].replace(/\'/g, '')).to.equal(rule); 99 | }); 100 | } else { 101 | expect(directiveKeys.indexOf(key)).to.equal(-1); 102 | } 103 | }); 104 | }); 105 | } 106 | 107 | function hash(str) { 108 | var h = crypto.createHash('sha256'); 109 | h.update(str, 'utf8'); 110 | return h.digest('base64'); 111 | } 112 | 113 | describe('express-csp', function () { 114 | describe('as an Express extension', function () { 115 | var app = express(); 116 | 117 | it('has an extend() function', function () { 118 | expect(csp).to.have.property('extend') 119 | .that.is.a('function'); 120 | }); 121 | 122 | it('sets a brand on the application', function () { 123 | csp.extend(app); 124 | 125 | expect(app).to.have.property('@csp') 126 | .that.equals(csp); 127 | }); 128 | 129 | it('adds methods to sign scripts', function () { 130 | expect(app).to.have.property('signScript') 131 | .that.is.a('function'); 132 | }); 133 | 134 | it('is only applied once', function () { 135 | var method = app.signScript; 136 | csp.extend(app); 137 | expect(app.signScript).to.equal(method); 138 | }); 139 | }); 140 | 141 | describe('an extended application', function () { 142 | var app = createApp(); 143 | 144 | it('sends basic CSP headers', function (done) { 145 | request(app).get('/') 146 | .expect(function (res) { 147 | expect(res.headers).to.have.property('content-security-policy'); 148 | }) 149 | .end(done); 150 | }); 151 | 152 | it('does not do anything without configuration', function (done) { 153 | var emptyApp = express(); 154 | csp.extend(emptyApp, {}); 155 | 156 | emptyApp.route('/').get(function (req, res) { 157 | res.writeHead(200, { 158 | 'Content-Type': 'text/html' 159 | }); 160 | res.end('CSP test'); 161 | }); 162 | 163 | request(emptyApp).get('/') 164 | .expect(function (res) { 165 | if (res.headers['content-security-policy']) { 166 | return 'Unexpected CSP header: ' + res.headers['content-security-policy']; 167 | } 168 | }) 169 | .end(done); 170 | }); 171 | }); 172 | 173 | describe('signs scripts and styles correctly', function () { 174 | var app = createApp(); 175 | var script1 = 'foo()'; 176 | var script2 = 'bar();'; 177 | var style1 = 'body{background:#fff}'; 178 | var style2 = 'body{border:1px solid #000}'; 179 | var style3 = 'p{background:#911d21}'; 180 | 181 | var script2hash = hash(script2); 182 | var style2hash = hash(style2); 183 | 184 | app.signScript(script1); 185 | app.signStyle(style1); 186 | app.signStyle(style3); 187 | 188 | app.route('/foo').get(function (req, res) { 189 | expect(res).to.have.property('signScript') 190 | .that.is.a('function'); 191 | 192 | res.signScript(script2); 193 | 194 | res.writeHead(200, { 195 | 'Content-Type': 'text/html' 196 | }); 197 | res.end('CSP test bar'); 198 | }); 199 | 200 | app.route('/baz').get(function (req, res) { 201 | res.signStyle(style2); 202 | 203 | res.writeHead(200, { 204 | 'Content-Type': 'text/html' 205 | }); 206 | res.end('CSP test bar'); 207 | }); 208 | 209 | specify('for application shared scripts', function (done) { 210 | request(app).get('/') 211 | .expect(function (res) { 212 | var policies = res.headers['content-security-policy'].split(';'); 213 | var hashedScripts = policies.filter(function (policy) { 214 | return policy.split(' ')[0] === 'script-src'; 215 | }); 216 | 217 | hashedScripts = hashedScripts[0].split(' ').slice(3); 218 | expect(hashedScripts.length).to.equal(1); 219 | expect(hashedScripts).to.contain('\'' + 'sha256-' + hash(script1) + '\''); 220 | }) 221 | .end(done); 222 | }); 223 | 224 | specify('for application shared styles', function (done) { 225 | request(app).get('/') 226 | .expect(function (res) { 227 | var policies = res.headers['content-security-policy'].split(';'); 228 | var hashedStyles = policies.filter(function (policy) { 229 | return policy.split(' ')[0] === 'style-src'; 230 | }); 231 | 232 | hashedStyles = hashedStyles[0].split(' ').slice(1); 233 | expect(hashedStyles.length).to.equal(2); 234 | expect(hashedStyles).to.contain('\'' + 'sha256-' + hash(style1) + '\''); 235 | }) 236 | .end(done); 237 | }); 238 | 239 | specify('for route specific scripts', function (done) { 240 | request(app).get('/foo') 241 | .expect(function (res) { 242 | var policies = res.headers['content-security-policy'].split(';'); 243 | var hashedScripts = policies.filter(function (policy) { 244 | return policy.split(' ')[0] === 'script-src'; 245 | }); 246 | 247 | expect(hashedScripts.length).to.equal(1); 248 | 249 | hashedScripts = hashedScripts[0].split(' ').slice(3); 250 | 251 | expect(hashedScripts).to.contain('\'' + 'sha256-' + script2hash + '\''); 252 | expect(hashedScripts.length).to.equal(2); 253 | }) 254 | .end(done); 255 | }); 256 | 257 | specify('for route specific styles', function (done) { 258 | request(app).get('/baz') 259 | .expect(function (res) { 260 | var policies = res.headers['content-security-policy'].split(';'); 261 | var hashedStyles = policies.filter(function (policy) { 262 | return policy.split(' ')[0] === 'style-src'; 263 | }); 264 | 265 | expect(hashedStyles.length).to.equal(1); 266 | 267 | hashedStyles = hashedStyles[0].split(' ').slice(1); 268 | 269 | expect(hashedStyles).to.contain('\'' + 'sha256-' + style2hash + '\''); 270 | expect(hashedStyles.length).to.equal(3); 271 | }) 272 | .end(done); 273 | }); 274 | 275 | specify('always uses the same signature for the same script', function (done) { 276 | app.route('/bar').get(function (req, res) { 277 | res.signScript(script2); 278 | res.signScript(script2); 279 | 280 | res.writeHead(200, { 281 | 'Content-Type': 'text/html' 282 | }); 283 | res.end('CSP test bar'); 284 | }); 285 | 286 | request(app).get('/bar') 287 | .expect(function (res) { 288 | var policies = res.headers['content-security-policy'].split(';'); 289 | var hashedScripts = policies.filter(function (policy) { 290 | return policy.split(' ')[0] === 'script-src'; 291 | }); 292 | 293 | expect(hashedScripts.length).to.equal(1); 294 | 295 | hashedScripts = hashedScripts[0].split(' ').slice(3); 296 | 297 | expect(hashedScripts).to.contain('\'' + 'sha256-' + script2hash + '\''); 298 | expect(hashedScripts.length).to.equal(2); 299 | }) 300 | .end(done); 301 | }); 302 | }); 303 | 304 | describe('directives get set', function () { 305 | var app = createApp({ 306 | policy: { 307 | directives: allDirectives 308 | }, 309 | reportPolicy: { 310 | directives: allDirectives 311 | } 312 | }); 313 | 314 | Object.keys(allDirectives).forEach(function(directive, index) { 315 | it(directive + ' directive is in the `content-security-policy` header', function(done) { 316 | request(app).get('/') 317 | .expect(function (res) { 318 | var policies = res.headers['content-security-policy'].split(';'), 319 | policy = policies[index].split(' '); 320 | expect(policy.shift()).to.equal(directive); 321 | policy.forEach(function(rule, ruleIndex) { 322 | expect(rule.replace(/\'/g, '')).to.equal(allDirectives[directive][ruleIndex]); 323 | }); 324 | }).end(done); 325 | }); 326 | 327 | it(directive + ' report directive is in the `content-security-policy-report-only` header', function(done) { 328 | request(app).get('/') 329 | .expect(function (res) { 330 | var policies = res.headers['content-security-policy-report-only'].split(';'), 331 | policy = policies[index].split(' '); 332 | expect(policy.shift()).to.equal(directive); 333 | policy.forEach(function(rule, ruleIndex) { 334 | expect(rule.replace(/\'/g, '')).to.equal(allDirectives[directive][ruleIndex]); 335 | }); 336 | }).end(done); 337 | }); 338 | }); 339 | }); 340 | 341 | describe('csp polices cannot be altered after they are set', function() { 342 | var cspPolicies = getCleanPolicies(); 343 | var app = createApp(cspPolicies); 344 | 345 | Object.keys(cspPolicies).forEach(function(policyType) { 346 | cspPolicies[policyType].useScriptNonce = true; 347 | cspPolicies[policyType].useStyleNonce = true; 348 | cspPolicies[policyType].directives['default-src'] = ['*']; 349 | cspPolicies[policyType].directives['script-src'] = ['*']; 350 | cspPolicies[policyType].directives['style-src'].push('*'); 351 | cspPolicies[policyType].directives['report-uri'] = ['http://reports.nefarious.com/reports']; 352 | }); 353 | 354 | app.route('/foo').get(function (req, res) { 355 | res.writeHead(200, { 356 | 'Content-Type': 'text/html' 357 | }); 358 | res.end('CSP test bar'); 359 | }); 360 | 361 | it('the csp policies are unaffected by changing the original value', function(done) { 362 | request(app).get('/foo') 363 | .expect(testToEnsurePoliciesAreUnchanged) 364 | .end(done); 365 | }); 366 | 367 | }); 368 | 369 | 370 | describe('directives are quoted properly', function () { 371 | var app = createApp({ 372 | policy: { 373 | directives: allDirectives 374 | }, 375 | reportPolicy: { 376 | directives: allDirectives 377 | } 378 | }); 379 | 380 | specify('for none', function (done) { 381 | request(app).get('/') 382 | .expect(function (res) { 383 | var policies = res.headers['content-security-policy'].split(';'); 384 | var hashedScripts = policies.filter(function (policy) { 385 | return policy.split(' ')[0] === 'object-src'; 386 | }); 387 | 388 | hashedScripts = hashedScripts[0].split(' ').slice(1); 389 | expect(hashedScripts.length).to.equal(1); 390 | expect(hashedScripts).to.contain('\'none\''); 391 | }) 392 | .end(done); 393 | }); 394 | }); 395 | 396 | describe('nonce token middleware', function () { 397 | var app; 398 | function setupApp(app) { 399 | app.set('views', path.join(__dirname, 'fixtures/views/')); 400 | app.engine('handlebars', exphbs({ 401 | defaultLayout: 'main', 402 | layoutsDir: path.join(app.get('views'), 'layouts/') 403 | })); 404 | app.set('view engine', 'handlebars'); 405 | 406 | app.get('/bar', function (req, res) { 407 | res.render('test'); 408 | }); 409 | 410 | return app; 411 | } 412 | 413 | it('includes the token in res.locals when it already has script-src rules', function (done) { 414 | app = setupApp(createApp({ 415 | policy: { 416 | useScriptNonce: true 417 | } 418 | })); 419 | request(app).get('/bar') 420 | .expect(function (res) { 421 | var token = res.text.trim(); 422 | expect(token.length).to.be.above(0); 423 | 424 | var policy = res.headers['content-security-policy'].split(';') 425 | .filter(function (policy) { 426 | return policy.substr(0, 'script-src '.length) === 427 | 'script-src '; 428 | })[0]; 429 | 430 | policy = policy.split(' ').slice(1); 431 | 432 | var nonce = policy.filter(function (rule) { 433 | return rule.replace(/\'/g, '').substr(0, 'nonce-'.length) === 'nonce-'; 434 | })[0].replace(/\'/g, '').substr('nonce-'.length); 435 | 436 | expect(token.length).to.equal(nonce.length); 437 | expect(token).to.equal(nonce); 438 | }) 439 | .end(done); 440 | }); 441 | 442 | it('includes the token in res.locals when it does not already has script-src rules', function (done) { 443 | app = setupApp(createApp({ 444 | policy: { 445 | useScriptNonce: true, 446 | directives: { 447 | 'style-src': ['*'] 448 | } 449 | } 450 | })); 451 | request(app).get('/bar') 452 | .expect(function (res) { 453 | var token = res.text.trim(); 454 | expect(token.length).to.be.above(0); 455 | 456 | var policy = res.headers['content-security-policy'].split(';') 457 | .filter(function (policy) { 458 | return policy.substr(0, 'script-src '.length) === 459 | 'script-src '; 460 | })[0]; 461 | policy = policy.split(' ').slice(1); 462 | 463 | var nonce = policy.filter(function (rule) { 464 | return rule.replace(/\'/g, '').substr(0, 'nonce-'.length) === 'nonce-'; 465 | })[0].replace(/\'/g, '').substr('nonce-'.length); 466 | 467 | expect(token.length).to.equal(nonce.length); 468 | expect(token).to.equal(nonce); 469 | }) 470 | .end(done); 471 | }); 472 | 473 | it('includes a token when set in the config', function(done) { 474 | app = setupApp(createApp({ 475 | policy: { 476 | useScriptNonce: true 477 | } 478 | })); 479 | request(app).get('/bar') 480 | .expect(function (res) { 481 | var token = res.text.trim(); 482 | expect(token.length).to.be.above(0); 483 | 484 | var policy = res.headers['content-security-policy'].split(';') 485 | .filter(function (policy) { 486 | return policy.substr(0, 'script-src '.length) === 487 | 'script-src '; 488 | })[0]; 489 | 490 | policy = policy.split(' ').slice(1); 491 | 492 | var nonce = policy.filter(function (rule) { 493 | return rule.replace(/\'/g, '').substr(0, 'nonce-'.length) === 'nonce-'; 494 | })[0].replace(/\'/g, '').substr('nonce-'.length); 495 | 496 | expect(token.length).to.equal(nonce.length); 497 | expect(token).to.equal(nonce); 498 | 499 | }) 500 | .end(done); 501 | }); 502 | 503 | it('includes only one token in the header for each request', function(done) { 504 | request(app).get('/bar') 505 | .expect(function (res) { 506 | var policy = res.headers['content-security-policy'].split(';') 507 | .filter(function (policy) { 508 | return policy.substr(0, 'script-src '.length) === 509 | 'script-src '; 510 | })[0]; 511 | 512 | policy = policy.split(' ').slice(1); 513 | 514 | var nonce = policy.filter(function(rule) { 515 | return rule.replace(/\'/g, '').indexOf('nonce-') === 0; 516 | }); 517 | 518 | expect(nonce.length).to.equal(1); 519 | }) 520 | .end(done); 521 | }); 522 | }); 523 | 524 | describe('response.setPolicy', function () { 525 | var app = createApp({ 526 | policy: { 527 | directives: allDirectives 528 | }, 529 | reportPolicy: { 530 | directives: allDirectives 531 | } 532 | }); 533 | 534 | var responsePolicies = getCleanPolicies(); 535 | 536 | app.route('/baz').get(function (req, res) { 537 | res.setPolicy(responsePolicies); 538 | res.writeHead(200, { 'Content-Type': 'text/html' }); 539 | res.end('CSP test bar'); 540 | }); 541 | 542 | Object.keys(responsePolicies).forEach(function(policyType) { 543 | it('response ' + policyType + ' takes precedence over app level policy', function(done) { 544 | var cspHeader = policyType === 'reportPolicy' ? 'content-security-policy-report-only' : 'content-security-policy'; 545 | request(app).get('/baz') 546 | .expect(function (res) { 547 | var policies = res.headers[cspHeader].split(';'); 548 | expect(policies.length).to.equal(2); 549 | //allDirectives keys are in the same order as the VALID_DIRECTIVES array that is used in the 550 | //app to construct policies. 551 | Object.keys(allDirectives).filter(function(directive) { 552 | return Object.keys(responsePolicies[policyType].directives).indexOf(directive) > -1; 553 | }).forEach(function(item, index, arr) { 554 | var policy = policies[index].split(' '); 555 | var key = arr[index]; 556 | expect(policy.shift()).to.equal(key); 557 | policy.forEach(function(item, index) { 558 | expect(item.replace(/\'/g, '')).to.equal(responsePolicies[policyType].directives[key][index]); 559 | }); 560 | }); 561 | }) 562 | .end(done); 563 | }); 564 | }); 565 | 566 | app.route('/foo').get(function (req, res) { 567 | //set the response policy 568 | res.setPolicy(responsePolicies); 569 | 570 | Object.keys(responsePolicies).forEach(function (policyType) { 571 | responsePolicies[policyType].useScriptNonce = true; 572 | responsePolicies[policyType].useStyleNonce = true; 573 | responsePolicies[policyType].directives['default-src'] = ['*']; 574 | responsePolicies[policyType].directives['script-src'] = ['*']; 575 | responsePolicies[policyType].directives['style-src'].push('*'); 576 | responsePolicies[policyType].directives['report-uri'] = ['http://reports.nefarious.com/reports']; 577 | }); 578 | 579 | res.writeHead(200, { 'Content-Type': 'text/html' }); 580 | res.end('CSP test bar'); 581 | }); 582 | 583 | it('the cspPolicies set by response.setPolicy cannot be altered', function(done) { 584 | request(app).get('/foo') 585 | .expect(testToEnsurePoliciesAreUnchanged) 586 | .end(done); 587 | }); 588 | 589 | app.route('/bar').get(function (req, res) { 590 | res.setPolicy(getCleanPolicies()); 591 | 592 | Object.keys(responsePolicies).forEach(function(policyType) { 593 | res.locals.cspPolicies[policyType].useScriptNonce = true; 594 | res.locals.cspPolicies[policyType].useStyleNonce = true; 595 | res.locals.cspPolicies[policyType].directives['default-src'] = ['*']; 596 | res.locals.cspPolicies[policyType].directives['script-src'] = ['*']; 597 | 598 | //attempting to update an immutable array should throw an error 599 | try { 600 | res.locals.cspPolicies[policyType].directives['style-src'].push('*'); 601 | } catch(e) { 602 | } 603 | res.locals.cspPolicies[policyType].directives['report-uri'] = ['http://reports.nefarious.com/reports']; 604 | }); 605 | 606 | // signing a script will force the headers to be reset 607 | res.signScript('console.log("bar");'); 608 | 609 | res.writeHead(200, { 'Content-Type': 'text/html' }); 610 | res.end('CSP test bar'); 611 | }); 612 | 613 | it('the cspPolicies set by response.setPolicy cannot be changed by altering res.locals.cspPolicies', function(done) { 614 | request(app).get('/bar') 615 | .expect(testToEnsurePoliciesAreUnchanged) 616 | .end(done); 617 | }); 618 | }); 619 | 620 | describe('manually set nonces and shas', function() { 621 | var policies = getCleanPolicies(), 622 | sha = 'sha256-' + hash('console.log("I am safe");'), 623 | app; 624 | policies.policy.directives['script-src'].push(sha); 625 | app = createApp(policies); 626 | 627 | app.route('/').get(function (req, res) { 628 | res.writeHead(200, { 629 | 'Content-Type': 'text/html' 630 | }); 631 | res.end('Manual nonce and sha test'); 632 | }); 633 | 634 | it('shas can be manually set at the app level', function(done) { 635 | request(app).get('/') 636 | .expect(function(res) { 637 | var policies = res.headers['content-security-policy'].split(';'); 638 | var scriptRules = policies.filter(function (policy) { 639 | return policy.split(' ')[0] === 'script-src'; 640 | })[0].split(' '); 641 | expect(scriptRules).to.contain('\'' + sha + '\''); 642 | }) 643 | .end(done); 644 | }); 645 | 646 | it('shas and nonces can be manually set at the response level', function(done) { 647 | var policies = getCleanPolicies(), 648 | nonce = 'nonce-foo1bar2baz3'; 649 | app.route('/foo').get(function (req, res) { 650 | policies.policy.directives['script-src'].push(sha); 651 | policies.policy.directives['script-src'].push(nonce); 652 | res.setPolicy(policies); 653 | res.writeHead(200, { 654 | 'Content-Type': 'text/html' 655 | }); 656 | res.end('Manual nonce and sha test'); 657 | }); 658 | request(app).get('/foo') 659 | .expect(function(res) { 660 | var policies = res.headers['content-security-policy'].split(';'); 661 | var scriptRules = policies.filter(function (policy) { 662 | return policy.split(' ')[0] === 'script-src'; 663 | })[0].split(' '); 664 | expect(scriptRules).to.contain('\'' + sha + '\''); 665 | expect(scriptRules).to.contain('\'' + nonce + '\''); 666 | }) 667 | .end(done); 668 | 669 | }); 670 | 671 | it('an error is thrown when a nonce is manually set at the app level', function() { 672 | var policies = getCleanPolicies(), 673 | nonce = 'nonce-foo1bar2baz3', 674 | app, 675 | error; 676 | policies.policy.directives['script-src'].push(nonce); 677 | try { 678 | app = createApp(policies); 679 | } catch(e) { 680 | error = e; 681 | } 682 | expect(error).to.not.be.an('undefined'); 683 | expect(error.message).to.not.be.an('undefined'); 684 | expect(error.message).to.equal("You cannot explicitly set a nonce at the app level. If you want to use a nonce, set `useScriptNonce` or `useStyleNonce` to true in the config object."); 685 | }); 686 | 687 | }); 688 | }); 689 | --------------------------------------------------------------------------------