diff --git a/lib/middleware/csp.js b/lib/middleware/csp.js
index 944aa1df..45d3abf3 100644
--- a/lib/middleware/csp.js
+++ b/lib/middleware/csp.js
@@ -1,80 +1,93 @@
-const url = require("url");
-const querystring = require("querystring");
-
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;
+const rPolicy = /^([-_a-zA-Z0-9]+)(:report-only|:ro)?$/i;
+
+function addHeader(res, header, value) {
+ const current = res.get(header);
+ if ( current == null ) {
+ res.set(header, value);
+ } else if ( Array.isArray(current) ) {
+ res.set(header, [...current, value]);
+ } else {
+ res.set(header, [current, value]);
+ }
+}
function createMiddleware(sCspUrlParameterName, oConfig) {
const {
allowDynamicPolicySelection = false,
allowDynamicPolicyDefinition = false,
- defaultPolicyIsReportOnly = false
+ defaultPolicy = "default",
+ defaultPolicyIsReportOnly = false,
+ defaultPolicy2 = null,
+ defaultPolicy2IsReportOnly = false,
+ definedPolicies = {}
} = oConfig;
return function csp(req, res, next) {
- let oPolicy;
- let bReportOnly = defaultPolicyIsReportOnly;
-
- if (req.method === "POST" &&
- req.headers["content-type"] === "application/csp-report" &&
- req.url.endsWith("/dummy.csplog")
- ) {
- // In report-only mode there must be a report-uri defined
- // For now just ignore the violation. It will be logged in the browser anyway.
+ if (req.method === "POST" ) {
+ if (req.headers["content-type"] === "application/csp-report" &&
+ req.path.endsWith("/dummy.csplog") ) {
+ // In report-only mode there must be a report-uri defined
+ // For now just ignore the violation. It will be logged in the browser anyway.
+ return;
+ }
+ next();
return;
}
- // 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"]
- };
+ // add CSP headers only to get requests for *.html pages
+ if (req.method !== "GET" || !req.path.endsWith(".html")) {
+ next();
+ return;
}
- const oParsedUrl = url.parse(req.url);
- const oQuery = querystring.parse(oParsedUrl.query);
- let sCspUrlParameterValue = oQuery[sCspUrlParameterName];
+ // If default policies are defined, they will even be send without a present URL parameter.
+ let policy = defaultPolicy && definedPolicies[defaultPolicy];
+ let reportOnly = defaultPolicyIsReportOnly;
+ const policy2 = defaultPolicy2 && definedPolicies[defaultPolicy2];
+ const reportOnly2 = defaultPolicy2IsReportOnly;
+ const sCspUrlParameterValue = req.query[sCspUrlParameterName];
if (sCspUrlParameterValue) {
const 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;
+ if (mPolicyMatch) {
+ if (allowDynamicPolicySelection) {
+ policy = definedPolicies[mPolicyMatch[1]];
+ reportOnly = mPolicyMatch[2] !== undefined;
+ } // else: ignore parameter
} 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);
+ if ( sCspUrlParameterValue.endsWith(":report-only") ) {
+ policy = sCspUrlParameterValue.slice(0, - ":report-only".length);
+ reportOnly = true;
+ } else if ( sCspUrlParameterValue.endsWith(":ro") ) {
+ policy = sCspUrlParameterValue.slice(0, - ":ro".length);
+ reportOnly = true;
+ } else {
+ policy = sCspUrlParameterValue;
+ reportOnly = false;
}
- oPolicy = {
- name: "dynamic-custom-policy",
- policy: sCspUrlParameterValue
- };
- }
+ } // else: parameter ignored
}
- if (oPolicy) {
- const sHeader = bReportOnly ? HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : HEADER_CONTENT_SECURITY_POLICY;
- let sHeaderValue;
-
- if (bReportOnly) {
+ // collect header values based on configuration
+ if (policy) {
+ if (reportOnly) {
// Add dummy report-uri. This is mandatory for the report-only mode.
- sHeaderValue = oPolicy.policy + " report-uri dummy.csplog;";
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy + " report-uri dummy.csplog;");
} else {
- sHeaderValue = oPolicy.policy;
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY, policy);
+ }
+ }
+ if (policy2) {
+ if (reportOnly2) {
+ // Add dummy report-uri. This is mandatory for the report-only mode.
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy2 + " report-uri dummy.csplog;");
+ } else {
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY, policy2);
}
-
- // Send response with CSP header
- res.removeHeader(HEADER_CONTENT_SECURITY_POLICY);
- res.removeHeader(HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY);
- res.setHeader(sHeader, sHeaderValue);
}
next();
diff --git a/lib/server.js b/lib/server.js
index c520302c..e14d971e 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -109,7 +109,8 @@ module.exports = {
* h2
-flag and a close
function,
* which can be used to stop the server.
*/
- serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, acceptRemoteConnections = false}) {
+ serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, acceptRemoteConnections = false,
+ cspDefaults = false}) {
return Promise.resolve().then(() => {
const projectResourceCollections = resourceFactory.createCollectionsForTree(tree);
@@ -134,7 +135,6 @@ module.exports = {
const oCspConfig = {
allowDynamicPolicySelection: true,
allowDynamicPolicyDefinition: true,
- defaultPolicyIsReportOnly: true,
definedPolicies: {
"sap-target-level-1":
"default-src 'self'; " +
@@ -156,6 +156,14 @@ module.exports = {
"connect-src 'self' https: wss:;"
}
};
+ if ( cspDefaults ) {
+ Object.assign(oCspConfig, {
+ defaultPolicy: "sap-target-level-1",
+ defaultPolicyIsReportOnly: true,
+ defaultPolicy2: "sap-target-level-2",
+ defaultPolicy2IsReportOnly: true,
+ });
+ }
app.use(csp("sap-ui-xx-csp-policy", oCspConfig));
app.use(compression());
diff --git a/test/lib/server/main.js b/test/lib/server/main.js
index 151e2377..778396e0 100644
--- a/test/lib/server/main.js
+++ b/test/lib/server/main.js
@@ -293,51 +293,170 @@ test("Stop server", (t) => {
test("CSP", (t) => {
return Promise.all([
+ request.get("/index.html").then((res) => {
+ t.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing csp header");
+ t.is(res.headers["content-security-policy-report-only"], undefined,
+ "response must not have report-only csp header");
+ }),
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.truthy(res.headers["content-security-policy"], "response should have enforcing csp header");
t.regex(res.headers["content-security-policy"], /script-src\s+'self'\s+'unsafe-eval'\s*;/,
- "policy should should have the expected content");
+ "header should should have the expected content");
t.is(res.headers["content-security-policy-report-only"], undefined,
- "response must not have csp report-only header");
+ "response must not have report-only csp 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.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing 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");
+ "header 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.truthy(res.headers["content-security-policy"], "response should have enforcing csp header");
t.regex(res.headers["content-security-policy"], /script-src\s+'self'\s*;/,
- "policy should should have the expected content");
+ "header should should have the expected content");
t.is(res.headers["content-security-policy-report-only"], undefined,
- "response must not have csp report-only header");
+ "response must not have report-only csp 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.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing 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");
+ "header 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");
+ request.get("/index.html?sap-ui-xx-csp-policy=default-src%20http%3a;").then((res) => {
+ t.truthy(res.headers["content-security-policy"], "response should have enforcing csp header");
+ t.regex(res.headers["content-security-policy"], /default-src\s+http:\s*;/,
+ "header should contain the configured policy");
t.is(res.headers["content-security-policy-report-only"], undefined,
- "response must not have csp report-only header");
+ "response must not have report-only csp 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");
+ request.get("/index.html?sap-ui-xx-csp-policy=default-src%20http%3a;:report-only").then((res) => {
+ t.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing 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");
+ t.regex(res.headers["content-security-policy-report-only"], /default-src\s+http:\s*;/,
+ "header should contain the configured policy");
+ }),
+ request.get("/index.html?sap-ui-xx-csp-policy=default-src%20http%3a;:ro").then((res) => {
+ t.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing 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+http:\s*;/,
+ "header should contain the configured policy");
})
]);
});
+/*
+ * Note: the '--csp-defaults' configuration sends two 'content-security-policy-report-only' headers.
+ * The response object of supertest joins the values of the two headers in a single string, which makes
+ * assertions below a bit harder to understand (two checks with different regex on the same header)
+ */
+test("CSP Defaults", (t) => {
+ const port = 3400;
+ const request = supertest(`http://localhost:${port}`);
+ let localServeResult;
+ return normalizer.generateProjectTree({
+ cwd: "./test/fixtures/application.a"
+ }).then((tree) => {
+ return server.serve(tree, {
+ port,
+ cspDefaults: true
+ });
+ }).then((serveResult) => {
+ localServeResult = serveResult;
+ return Promise.all([
+ request.get("/index.html").then((res) => {
+ t.is(res.headers["content-security-policy"], undefined, "response must not have enforcing 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*;/,
+ "header should contain the 1st default policy");
+ t.regex(res.headers["content-security-policy-report-only"], /script-src\s+'self'\s*;/,
+ "header should contain the 2nd default policy");
+ }),
+ 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 enforcing csp header");
+ t.regex(res.headers["content-security-policy"], /script-src\s+'self'\s+'unsafe-eval'\s*;/,
+ "header should should have the expected content");
+ 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*;/,
+ "header should contain the 2nd default policy");
+ }),
+ 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 enforcing 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*;/,
+ "header should should have the expected content");
+ t.regex(res.headers["content-security-policy-report-only"], /script-src\s+'self'\s*;/,
+ "header should contain the 2nd default policy");
+ }),
+ 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 enforcing csp header");
+ t.regex(res.headers["content-security-policy"], /script-src\s+'self'\s*;/,
+ "header should should have the expected content");
+ t.regex(res.headers["content-security-policy-report-only"], /script-src\s+'self'\s*;/,
+ "header should contain the 2nd default policy");
+ }),
+ 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 enforcing 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*;/,
+ "header should have the expected content");
+ }),
+ request.get("/index.html?sap-ui-xx-csp-policy=default-src%20http%3a;").then((res) => {
+ t.truthy(res.headers["content-security-policy"], "response should have enforcing csp header");
+ t.regex(res.headers["content-security-policy"], /default-src\s+http:\s*;/,
+ "header should contain the configured policy");
+ t.regex(res.headers["content-security-policy-report-only"], /script-src\s+'self'\s*;/,
+ "header should contain the 2nd default policy");
+ }),
+ request.get("/index.html?sap-ui-xx-csp-policy=default-src%20http%3a;:report-only").then((res) => {
+ t.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing 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+http:\s*;/,
+ "header should contain the configured policy");
+ t.regex(res.headers["content-security-policy-report-only"], /default-src\s+'self'\s*;/,
+ "header should contain the 2nd default policy");
+ }),
+ request.get("/index.html?sap-ui-xx-csp-policy=default-src%20http%3a;:ro").then((res) => {
+ t.is(res.headers["content-security-policy"], undefined,
+ "response must not have enforcing 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+http:\s*;/,
+ "header should contain the configured policy");
+ t.regex(res.headers["content-security-policy-report-only"], /default-src\s+'self'\s*;/,
+ "header should contain the 2nd default policy");
+ })
+ ]);
+ }).then(() => {
+ return new Promise((resolve, reject) => {
+ localServeResult.close((error) => {
+ if (error) {
+ reject(error);
+ } else {
+ t.pass("Server closing");
+ resolve();
+ }
+ });
+ });
+ });
+});
+
test("Get index of resources", (t) => {
return Promise.all([
request.get("").then((res) => {
diff --git a/test/lib/server/middleware/csp.js b/test/lib/server/middleware/csp.js
new file mode 100644
index 00000000..8708610c
--- /dev/null
+++ b/test/lib/server/middleware/csp.js
@@ -0,0 +1,146 @@
+const {test} = require("ava");
+const cspMiddleware = require("../../../../lib/middleware/csp");
+
+test("Default Settings", (t) => {
+ t.plan(3 + 7); // fourth request should end in middleware and not call next!
+ const middleware = cspMiddleware("sap-ui-xx-csp-policy", {});
+ const res = {
+ get: function() {
+ return undefined;
+ },
+ set: function(header, value) {
+ t.fail(`should not be called with header ${header} and value ${value}`);
+ }
+ };
+ const next = function() {
+ t.pass("Next was called.");
+ };
+ const noNext = function() {
+ t.fail("Next should not be called");
+ };
+
+ middleware({method: "GET", path: "/test.html", headers: {}, query: {}}, res, next);
+ middleware({
+ method: "GET",
+ path: "/test.html",
+ headers: {},
+ query: {
+ "sap-ui-xx-csp-policy": "sap-target-level-2"
+ }
+ }, res, next);
+ middleware({method: "POST", path: "somePath", headers: {}, query: {}}, res, next);
+ middleware({
+ method: "POST",
+ path: "/dummy.csplog",
+ headers: {"content-type": "application/csp-report"},
+ query: {}
+ }, res, noNext);
+
+ // check that unsupported methods result in a call to next()
+ ["CONNECT", "DELETE", "HEAD", "OPTIONS", "PATCH", "PUT", "TRACE"].forEach(
+ (method) => middleware({method, path: "/dummy.csplog", headers: {}, query: {}}, res, next)
+ );
+});
+
+test("Custom Settings", (t) => {
+ const middleware = cspMiddleware("csp", {
+ definedPolicies: {
+ policy1: "default-src 'self';",
+ policy2: "default-src http:;",
+ policy3: "default-src https:;"
+ },
+ defaultPolicy: "policy1",
+ defaultPolicyIsReportOnly: false,
+ defaultPolicy2: "policy2",
+ defaultPolicy2IsReportOnly: false,
+ allowDynamicPolicySelection: true,
+ allowDynamicPolicyDefinition: true
+ });
+ let expected;
+ const res = {
+ get: function() {
+ return undefined;
+ },
+ set: function(header, value) {
+ if ( header.toLowerCase() === "content-security-policy" ) {
+ t.is(value, expected.shift(), "should have the expected value");
+ } else {
+ t.fail(`should not be called with header ${header} and value ${value}`);
+ }
+ }
+ };
+ const next = function() {
+ t.pass("Next was called.");
+ };
+
+ expected = ["default-src 'self';", "default-src http:;"];
+ middleware({method: "GET", path: "/test.html", headers: {}, query: {}}, res, next);
+
+ expected = ["default-src https:;", "default-src http:;"];
+ middleware({method: "GET", path: "/test.html", headers: {}, query: {"csp": "policy3"}}, res, next);
+
+ expected = ["default-src ftp:;", "default-src http:;"];
+ middleware({method: "GET", path: "/test.html", headers: {}, query: {"csp": "default-src ftp:;"}}, res, next);
+});
+
+test("No Dynamic Policy Definition", (t) => {
+ const middleware = cspMiddleware("csp", {
+ definedPolicies: {
+ policy1: "default-src 'self';",
+ policy2: "default-src http:;"
+ },
+ defaultPolicy: "policy1",
+ defaultPolicyIsReportOnly: false,
+ defaultPolicy2: "policy2",
+ defaultPolicy2IsReportOnly: false,
+ allowDynamicPolicyDefinition: false
+ });
+ const res = {
+ get: function() {
+ return undefined;
+ },
+ set: function(header, value) {
+ if ( header.toLowerCase() === "content-security-policy" ) {
+ t.is(value, expected.shift(), "should have the expected value");
+ } else {
+ t.fail(`should not be called with header ${header} and value ${value}`);
+ }
+ }
+ };
+ const next = function() {
+ t.pass("Next was called.");
+ };
+
+ const expected = ["default-src 'self';", "default-src http:;"];
+ middleware({method: "GET", path: "/test.html", headers: {}, query: {"csp": "default-src ftp:;"}}, res, next);
+});
+
+test("Header Manipulation", (t) => {
+ const middleware = cspMiddleware("csp", {
+ definedPolicies: {
+ policy1: "default-src 'self';",
+ policy2: "default-src http:;"
+ },
+ defaultPolicy: "policy1",
+ defaultPolicyIsReportOnly: false,
+ defaultPolicy2: "policy2",
+ defaultPolicy2IsReportOnly: false
+ });
+ let cspHeader = "default-src: spdy:";
+ const res = {
+ get: function() {
+ return cspHeader;
+ },
+ set: function(header, value) {
+ if ( header.toLowerCase() === "content-security-policy" ) {
+ cspHeader = value;
+ } else {
+ t.fail(`should not be called with header ${header} and value ${value}`);
+ }
+ }
+ };
+ const next = function() {};
+
+ middleware({method: "GET", path: "/test.html", headers: {}, query: {}}, res, next);
+ t.deepEqual(cspHeader, ["default-src: spdy:", "default-src 'self';", "default-src http:;"]);
+});