From f8bfc5a8bf64cc6eac9b27258b7471a7db7c5ca9 Mon Sep 17 00:00:00 2001 From: Dominik Garrecht Date: Wed, 4 Apr 2018 17:49:10 +0200 Subject: [PATCH 1/4] [FEATURE] Add CSP middleware --- lib/middleware/csp.js | 71 +++++++++++++++++++++++++++++++++++++++++++ lib/server.js | 14 +++++++++ 2 files changed, 85 insertions(+) create mode 100644 lib/middleware/csp.js diff --git a/lib/middleware/csp.js b/lib/middleware/csp.js new file mode 100644 index 00000000..7e917e07 --- /dev/null +++ b/lib/middleware/csp.js @@ -0,0 +1,71 @@ +var url = require('url'); +var querystring = require('querystring'); + +var HEADER_CONTENT_SECURITY_POLICY = "Content-Security-Policy", + HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only", + rPolicy = /([-_a-zA-Z0-9]+)(:report-only)?/i; + +function createMiddleware(sCspUrlParameterName, oConfig) { + return function csp(req, res, next) { + var allowDynamicPolicySelection = oConfig.allowDynamicPolicySelection || false, + allowDynamicPolicyDefinition = oConfig.allowDynamicPolicyDefinition || false, + defaultPolicyIsReportOnly = oConfig.defaultPolicyIsReportOnly || false, + sHeader, + sHeaderValue, + sCspUrlParameterValue, + oParsedUrl, + oQuery, + oPolicy, + mPolicyMatch, + bReportOnly = defaultPolicyIsReportOnly; + + // If a policy with name 'default' is defined, it will even be send without a present URL parameter. + if (oConfig.definedPolicies["default"]) { + oPolicy = { + name: "default", + policy: oConfig.definedPolicies["default"] + }; + } + + // Use random protocol, host and port to establish a valid URL for parsing query parameters + oParsedUrl = url.parse(req.url); + oQuery = querystring.parse(oParsedUrl.query); + sCspUrlParameterValue = oQuery[sCspUrlParameterName]; + + if (sCspUrlParameterValue) { + mPolicyMatch = rPolicy.exec(sCspUrlParameterValue); + + if (mPolicyMatch && mPolicyMatch[1] && oConfig.definedPolicies[mPolicyMatch[1]] && allowDynamicPolicySelection) { + oPolicy = { + name: mPolicyMatch[1], + policy: oConfig.definedPolicies[mPolicyMatch[1]] + }; + bReportOnly = mPolicyMatch[2] !== undefined; + } else if (allowDynamicPolicyDefinition) { + // Custom CSP policy directives get passed as part of the CSP URL-Parameter value + bReportOnly = sCspUrlParameterValue.endsWith(":report-only"); + if (bReportOnly) { + sCspUrlParameterValue = sCspUrlParameterValue.substr(0, sCspUrlParameterValue.length - ":report-only".length); + } + oPolicy = { + name: "dynamic-custom-policy", + policy: sCspUrlParameterValue + }; + } + } + + if (oPolicy) { + sHeader = bReportOnly ? HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : HEADER_CONTENT_SECURITY_POLICY; + sHeaderValue = oPolicy.policy; + + // Send response with CSP header + res.removeHeader(HEADER_CONTENT_SECURITY_POLICY); + res.removeHeader(HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY); + res.setHeader(sHeader, sHeaderValue); + } + + next(); + }; +} + +module.exports = createMiddleware; diff --git a/lib/server.js b/lib/server.js index a53a3d88..c0b1b80e 100644 --- a/lib/server.js +++ b/lib/server.js @@ -8,6 +8,7 @@ const serveResources = require("./middleware/serveResources"); const discovery = require("./middleware/discovery"); const versionInfo = require("./middleware/versionInfo"); const serveThemes = require("./middleware/serveThemes"); +const csp = require("./middleware/csp"); const ui5connect = require("connect-openui5"); const nonReadRequests = require("./middleware/nonReadRequests"); const ui5Fs = require("@ui5/fs"); @@ -50,6 +51,19 @@ function serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, ac }; const app = express(); + + var oCspConfig = { + allowDynamicPolicySelection: true, + allowDynamicPolicyDefinition: true, + defaultPolicyIsReportOnly: true, + definedPolicies: { + "detailed-directives": "default-src 'none'; script-src 'self'; frame-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; style-src 'self' 'unsafe-inline';", + "almost-default": "default-src 'self'; script-src 'self'; style-src 'unsafe-inline' *;", + "ui5-working": "default-src 'self'; script-src 'unsafe-eval' * ; style-src 'unsafe-inline' * ;" + } + }; + app.use(csp("sap-ui-xx-csp-policy", oCspConfig)); + app.use(compression()); app.use(cors()); From 258173543c236e27cfad7ef6d9eb19f70d12cf80 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Wed, 4 Jul 2018 16:19:19 +0200 Subject: [PATCH 2/4] [INTERNAL] Update policies, fix lint errors, use more ES6 --- lib/middleware/csp.js | 114 +++++++++++++++++++++--------------------- lib/server.js | 21 ++++++-- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/lib/middleware/csp.js b/lib/middleware/csp.js index 7e917e07..b0a1b474 100644 --- a/lib/middleware/csp.js +++ b/lib/middleware/csp.js @@ -1,71 +1,69 @@ -var url = require('url'); -var querystring = require('querystring'); +const url = require("url"); +const querystring = require("querystring"); -var HEADER_CONTENT_SECURITY_POLICY = "Content-Security-Policy", - HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only", - rPolicy = /([-_a-zA-Z0-9]+)(:report-only)?/i; +const HEADER_CONTENT_SECURITY_POLICY = "Content-Security-Policy"; +const HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only"; +const rPolicy = /([-_a-zA-Z0-9]+)(:report-only)?/i; function createMiddleware(sCspUrlParameterName, oConfig) { - return function csp(req, res, next) { - var allowDynamicPolicySelection = oConfig.allowDynamicPolicySelection || false, - allowDynamicPolicyDefinition = oConfig.allowDynamicPolicyDefinition || false, - defaultPolicyIsReportOnly = oConfig.defaultPolicyIsReportOnly || false, - sHeader, - sHeaderValue, - sCspUrlParameterValue, - oParsedUrl, - oQuery, - oPolicy, - mPolicyMatch, - bReportOnly = defaultPolicyIsReportOnly; + const { + allowDynamicPolicySelection=false, + allowDynamicPolicyDefinition=false, + defaultPolicyIsReportOnly=false + } = oConfig; - // If a policy with name 'default' is defined, it will even be send without a present URL parameter. - if (oConfig.definedPolicies["default"]) { - oPolicy = { - name: "default", - policy: oConfig.definedPolicies["default"] - }; - } + return function csp(req, res, next) { + let oPolicy; + let bReportOnly = defaultPolicyIsReportOnly; - // Use random protocol, host and port to establish a valid URL for parsing query parameters - oParsedUrl = url.parse(req.url); - oQuery = querystring.parse(oParsedUrl.query); - sCspUrlParameterValue = oQuery[sCspUrlParameterName]; + // If a policy with name 'default' is defined, it will even be send without a present URL parameter. + if (oConfig.definedPolicies["default"]) { + oPolicy = { + name: "default", + policy: oConfig.definedPolicies["default"] + }; + } - if (sCspUrlParameterValue) { - mPolicyMatch = rPolicy.exec(sCspUrlParameterValue); + // Use random protocol, host and port to establish a valid URL for parsing query parameters + let oParsedUrl = url.parse(req.url); + let oQuery = querystring.parse(oParsedUrl.query); + let sCspUrlParameterValue = oQuery[sCspUrlParameterName]; - if (mPolicyMatch && mPolicyMatch[1] && oConfig.definedPolicies[mPolicyMatch[1]] && allowDynamicPolicySelection) { - oPolicy = { - name: mPolicyMatch[1], - policy: oConfig.definedPolicies[mPolicyMatch[1]] - }; - bReportOnly = mPolicyMatch[2] !== undefined; - } else if (allowDynamicPolicyDefinition) { - // Custom CSP policy directives get passed as part of the CSP URL-Parameter value - bReportOnly = sCspUrlParameterValue.endsWith(":report-only"); - if (bReportOnly) { - sCspUrlParameterValue = sCspUrlParameterValue.substr(0, sCspUrlParameterValue.length - ":report-only".length); - } - oPolicy = { - name: "dynamic-custom-policy", - policy: sCspUrlParameterValue - }; - } - } + if (sCspUrlParameterValue) { + let mPolicyMatch = rPolicy.exec(sCspUrlParameterValue); - if (oPolicy) { - sHeader = bReportOnly ? HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : HEADER_CONTENT_SECURITY_POLICY; - sHeaderValue = oPolicy.policy; + if (mPolicyMatch && mPolicyMatch[1] + && oConfig.definedPolicies[mPolicyMatch[1]] && allowDynamicPolicySelection) { + oPolicy = { + name: mPolicyMatch[1], + policy: oConfig.definedPolicies[mPolicyMatch[1]] + }; + bReportOnly = mPolicyMatch[2] !== undefined; + } else if (allowDynamicPolicyDefinition) { + // Custom CSP policy directives get passed as part of the CSP URL-Parameter value + bReportOnly = sCspUrlParameterValue.endsWith(":report-only"); + if (bReportOnly) { + sCspUrlParameterValue = sCspUrlParameterValue.slice(0, - ":report-only".length); + } + oPolicy = { + name: "dynamic-custom-policy", + policy: sCspUrlParameterValue + }; + } + } - // Send response with CSP header - res.removeHeader(HEADER_CONTENT_SECURITY_POLICY); - res.removeHeader(HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY); - res.setHeader(sHeader, sHeaderValue); - } + if (oPolicy) { + let sHeader = bReportOnly ? HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : HEADER_CONTENT_SECURITY_POLICY; + let sHeaderValue = oPolicy.policy; - next(); - }; + // Send response with CSP header + res.removeHeader(HEADER_CONTENT_SECURITY_POLICY); + res.removeHeader(HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY); + res.setHeader(sHeader, sHeaderValue); + } + + next(); + }; } module.exports = createMiddleware; diff --git a/lib/server.js b/lib/server.js index c0b1b80e..b8599a01 100644 --- a/lib/server.js +++ b/lib/server.js @@ -57,9 +57,24 @@ function serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, ac allowDynamicPolicyDefinition: true, defaultPolicyIsReportOnly: true, definedPolicies: { - "detailed-directives": "default-src 'none'; script-src 'self'; frame-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; style-src 'self' 'unsafe-inline';", - "almost-default": "default-src 'self'; script-src 'self'; style-src 'unsafe-inline' *;", - "ui5-working": "default-src 'self'; script-src 'unsafe-eval' * ; style-src 'unsafe-inline' * ;" + "sap-target-level-1": + "default-src 'self'; " + + "script-src 'self' 'unsafe-eval'; " + + "style-src 'self' 'unsafe-inline'; " + + "font-src 'self' data:; " + + "img-src 'self' * data: blob:; " + + "frame-src 'self' https: data: blob:; " + + "child-src 'self' https: data: blob:; " + + "connect-src 'self' https: wss:;", + "sap-target-level-2": + "default-src 'self'; " + + "script-src 'self'; " + + "style-src 'self' 'unsafe-inline'; " + + "font-src 'self' data:; " + + "img-src 'self' * data: blob:; " + + "frame-src 'self' https: data: blob:; " + + "child-src 'self' https: data: blob:; " + + "connect-src 'self' https: wss:;" } }; app.use(csp("sap-ui-xx-csp-policy", oCspConfig)); From e42330cbdbe80ac45fcc23f24eb98edb623b6518 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Thu, 5 Jul 2018 07:13:06 +0200 Subject: [PATCH 3/4] Add unit tests for csp middleware --- test/lib/server.js | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/lib/server.js b/test/lib/server.js index d79f5c98..9f79937d 100644 --- a/test/lib/server.js +++ b/test/lib/server.js @@ -379,3 +379,50 @@ test("Start server twice - Port is already taken and the next one is used", (t) t.fail(error); }); }); + +test("CSP", (t) => { + return Promise.all([ + request.get("/index.html?sap-ui-xx-csp-policy=sap-target-level-1").then((res) => { + t.truthy(res.headers["content-security-policy"], "response should have csp header"); + t.regex(res.headers["content-security-policy"], /script-src\s+'self'\s+'unsafe-eval'\s*;/, + "policy should should have the expected content"); + t.is(res.headers["content-security-policy-report-only"], undefined, + "response must not have csp report-only header"); + }), + request.get("/index.html?sap-ui-xx-csp-policy=sap-target-level-1:report-only").then((res) => { + t.is(res.headers["content-security-policy"], undefined, "response must not have csp header"); + t.truthy(res.headers["content-security-policy-report-only"], + "response should have report-only csp header"); + t.regex(res.headers["content-security-policy-report-only"], /script-src\s+'self'\s+'unsafe-eval'\s*;/, + "policy should should have the expected content"); + }), + request.get("/index.html?sap-ui-xx-csp-policy=sap-target-level-2").then((res) => { + t.truthy(res.headers["content-security-policy"], "response should have csp header"); + t.regex(res.headers["content-security-policy"], /script-src\s+'self'\s*;/, + "policy should should have the expected content"); + t.is(res.headers["content-security-policy-report-only"], undefined, + "response must not have csp report-only header"); + }), + request.get("/index.html?sap-ui-xx-csp-policy=sap-target-level-2:report-only").then((res) => { + t.is(res.headers["content-security-policy"], undefined, "response must not have csp header"); + t.truthy(res.headers["content-security-policy-report-only"], + "response should have report-only csp header"); + t.regex(res.headers["content-security-policy-report-only"], /script-src\s+'self'\s*;/, + "policy should should have the expected content"); + }), + request.get("/index.html?sap-ui-xx-csp-policy=default-src%20'self';").then((res) => { + t.truthy(res.headers["content-security-policy"], "response should have csp header"); + t.regex(res.headers["content-security-policy"], /default-src\s+'self'\s*;/, + "policy should should have the expected content"); + t.is(res.headers["content-security-policy-report-only"], undefined, + "response must not have csp report-only header"); + }), + request.get("/index.html?sap-ui-xx-csp-policy=default-src%20'self';:report-only").then((res) => { + t.is(res.headers["content-security-policy"], undefined, "response must not have csp header"); + t.truthy(res.headers["content-security-policy-report-only"], + "response should have report-only csp header"); + t.regex(res.headers["content-security-policy-report-only"], /default-src\s+'self'\s*;/, + "policy should should have the expected content"); + }) + ]); +}); From 3d90ec7daa991bc3cec56678d66b0eec951af31d Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Thu, 5 Jul 2018 07:16:05 +0200 Subject: [PATCH 4/4] Fix yet another linting issue --- lib/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server.js b/lib/server.js index b8599a01..0ee07128 100644 --- a/lib/server.js +++ b/lib/server.js @@ -52,7 +52,7 @@ function serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, ac const app = express(); - var oCspConfig = { + const oCspConfig = { allowDynamicPolicySelection: true, allowDynamicPolicyDefinition: true, defaultPolicyIsReportOnly: true,