diff --git a/lib/middleware/MiddlewareManager.js b/lib/middleware/MiddlewareManager.js
index 537d70fd..259a240c 100644
--- a/lib/middleware/MiddlewareManager.js
+++ b/lib/middleware/MiddlewareManager.js
@@ -8,7 +8,8 @@ const MiddlewareUtil = require("./MiddlewareUtil");
*/
class MiddlewareManager {
constructor({tree, resources, options = {
- sendSAPTargetCSP: false
+ sendSAPTargetCSP: false,
+ serveCSPReports: false
}}) {
if (!tree || !resources || !resources.all || !resources.rootProject || !resources.dependencies) {
throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided");
@@ -118,6 +119,11 @@ class MiddlewareManager {
defaultPolicy2IsReportOnly: true,
});
}
+ if (this.options.serveCSPReports) {
+ Object.assign(oCspConfig, {
+ serveCSPReports: true,
+ });
+ }
return () => {
return cspModule("sap-ui-xx-csp-policy", oCspConfig);
};
diff --git a/lib/middleware/csp.js b/lib/middleware/csp.js
index 541a3094..0ab10817 100644
--- a/lib/middleware/csp.js
+++ b/lib/middleware/csp.js
@@ -1,6 +1,9 @@
const parseurl = require("parseurl");
+const Router = require("router");
const querystring = require("querystring");
+const log = require("@ui5/logger").getLogger("server:middleware:csp");
+
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|:ro)?$/i;
@@ -16,6 +19,27 @@ function addHeader(res, header, value) {
}
}
+
+/**
+ * @typedef {object} CspConfig
+ * @property {boolean} allowDynamicPolicySelection
+ * @property {boolean} allowDynamicPolicyDefinition
+ * @property {string} defaultPolicy
+ * @property {boolean} defaultPolicyIsReportOnly
+ * @property {string} defaultPolicy2
+ * @property {boolean} defaultPolicy2IsReportOnly
+ * @property {object} definedPolicies
+ * @property {boolean} serveCSPReports whether to serve the csp resources
+ */
+
+/**
+ * @module @ui5/server/middleware/csp
+ * Middleware which enables CSP (content security policy) support
+ * @see https://www.w3.org/TR/CSP/
+ * @param {string} sCspUrlParameterName
+ * @param {CspConfig} oConfig
+ * @returns {Function} Returns a server middleware closure.
+ */
function createMiddleware(sCspUrlParameterName, oConfig) {
const {
allowDynamicPolicySelection = false,
@@ -24,22 +48,65 @@ function createMiddleware(sCspUrlParameterName, oConfig) {
defaultPolicyIsReportOnly = false,
defaultPolicy2 = null,
defaultPolicy2IsReportOnly = false,
- definedPolicies = {}
+ definedPolicies = {},
+ serveCSPReports = false
} = oConfig;
- return function csp(req, res, next) {
- const oParsedURL = parseurl(req);
- if (req.method === "POST" ) {
- if (req.headers["content-type"] === "application/csp-report" &&
- oParsedURL.pathname.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.
+ /**
+ * List of CSP Report entries
+ */
+ const cspReportEntries = [];
+ const router = new Router();
+ // .csplog
+ // body parser is required to parse csp-report in body (json)
+ if (serveCSPReports) {
+ const bodyParser = require("body-parser");
+ router.post("/.ui5/csp/report.csplog", bodyParser.json({type: "application/csp-report"}));
+ }
+ router.post("/.ui5/csp/report.csplog", function(req, res, next) {
+ if (req.headers["content-type"] === "application/csp-report") {
+ if (!serveCSPReports) {
+ res.end();
+ return;
+ }
+ // Write the violation into an array
+ // They can be retrieved via a request to '/.ui5/csp/csp-reports.json'
+ if (typeof req.body !== "object") {
+ const error = new Error(`No body content available: ${req.url}`);
+ log.error(error);
+ next(error);
return;
}
+ const cspReportObject = req.body["csp-report"];
+ if (cspReportObject) {
+ // extract the csp-report and add it to the cspReportEntries list
+ cspReportEntries.push(cspReportObject);
+ }
+ res.end();
+ } else {
next();
- return;
}
+ });
+
+ // csp-reports.json
+ if (serveCSPReports) {
+ router.get("/.ui5/csp/csp-reports.json", (req, res, next) => {
+ // serve csp reports
+ const body = JSON.stringify({
+ "csp-reports": cspReportEntries
+ }, null, "\t");
+ res.writeHead(200, {
+ "Content-Type": "application/json"
+ });
+ res.end(body);
+ });
+ }
+
+ // html get requests
+ // add csp headers
+ router.use((req, res, next) => {
+ const oParsedURL = parseurl(req);
// add CSP headers only to get requests for *.html pages
if (req.method !== "GET" || !oParsedURL.pathname.endsWith(".html")) {
@@ -81,23 +148,25 @@ function createMiddleware(sCspUrlParameterName, oConfig) {
// collect header values based on configuration
if (policy) {
if (reportOnly) {
- // Add dummy report-uri. This is mandatory for the report-only mode.
- addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy + " report-uri dummy.csplog;");
+ // Add report-uri. This is mandatory for the report-only mode.
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy + " report-uri /.ui5/csp/report.csplog;");
} else {
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;");
+ // Add report-uri. This is mandatory for the report-only mode.
+ addHeader(res, HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY, policy2 + " report-uri /.ui5/csp/report.csplog;");
} else {
addHeader(res, HEADER_CONTENT_SECURITY_POLICY, policy2);
}
}
next();
- };
+ });
+
+ return router;
}
module.exports = createMiddleware;
diff --git a/lib/server.js b/lib/server.js
index 8cf1b0f4..56ee9a6d 100644
--- a/lib/server.js
+++ b/lib/server.js
@@ -111,6 +111,7 @@ module.exports = {
* aim for (AKA 'target policies'), are send for any requested
* *.html
file
* @param {boolean} [options.simpleIndex=false] Use a simplified view for the server directory listing
+ * @param {boolean} [options.serveCSPReports=false] Enable csp reports serving for request url '/.ui5/csp/csp-reports.json'
* @returns {Promise