Skip to content

Commit

Permalink
[FEATURE] Add CSP middleware (#3)
Browse files Browse the repository at this point in the history
Adds a Content Security Policy (CSP) middleware which can be enabled via URL parameter (sap-ui-xx-csp-policy). One can set the value to a policy string directly, or use one of the already defined policy IDs (sap-target-level-1, sap-target-level-2).

The OpenUI5 QUnit test "[sap/ui/core/qunit/csp/ContentSecurityPolicy.qunit.html](https://github.com/SAP/openui5/blob/master/src/sap.ui.core/test/sap/ui/core/qunit/csp/ContentSecurityPolicy.qunit.html)" can be used for a starting point validating for CSP compliance.
  • Loading branch information
domianer authored and matz3 committed Jul 6, 2018
1 parent f0b40f7 commit f9ec3ee
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
69 changes: 69 additions & 0 deletions lib/middleware/csp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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;

function createMiddleware(sCspUrlParameterName, oConfig) {
const {
allowDynamicPolicySelection=false,
allowDynamicPolicyDefinition=false,
defaultPolicyIsReportOnly=false
} = oConfig;

return function csp(req, res, next) {
let oPolicy;
let 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
let oParsedUrl = url.parse(req.url);
let oQuery = querystring.parse(oParsedUrl.query);
let sCspUrlParameterValue = oQuery[sCspUrlParameterName];

if (sCspUrlParameterValue) {
let 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.slice(0, - ":report-only".length);
}
oPolicy = {
name: "dynamic-custom-policy",
policy: sCspUrlParameterValue
};
}
}

if (oPolicy) {
let sHeader = bReportOnly ? HEADER_CONTENT_SECURITY_POLICY_REPORT_ONLY : HEADER_CONTENT_SECURITY_POLICY;
let 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;
29 changes: 29 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -51,6 +52,34 @@ function serve(tree, {port, changePortIfInUse = false, h2 = false, key, cert, ac
};

const app = express();

const oCspConfig = {
allowDynamicPolicySelection: true,
allowDynamicPolicyDefinition: true,
defaultPolicyIsReportOnly: true,
definedPolicies: {
"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));

app.use(compression());
app.use(cors());

Expand Down
47 changes: 47 additions & 0 deletions test/lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
})
]);
});

0 comments on commit f9ec3ee

Please sign in to comment.