Content Security Policy
Content Security Policy (CSP) is a browser standard that helps to mitigate certain types of attacks. Strict CSP allows the application to define rules for how JavaScript can be loaded and run in the browser. This provides an additional layer of security against Cross-Site Scripting (XSS), data injection, data theft, site defacement and malware distribution.
Vaadin & Strict CSP
Vaadin Flow isn’t generally compatible with strict CSP rules. This is due to how the client-server communication has been built. For example, dynamic JavaScript functions and eval
calls are used in Vaadin. These aren’t compatible with strict CSP rules.
However, with some effort, nonce-based strict CSP can be used with Vaadin Flow applications. Nonce is a random identifier which is generated by the server for each HTTP request. It needs to be included in the response headers. Once this is done, only script tags containing this nonce value are loaded by the browser — while dynamic functions and calling eval
are not allowed.
Strict CSP implemented this way is only available when the Vaadin application is running in production mode.
Creating Random Nonce
To create the random nonce value, the application needs to have an IndexHtmlRequestListener
, which must generate and add the nonce to the index file response and all script tags, like this for example:
String nonce = UUID.randomUUID().toString();
// Add a header to make the browser require the nonce in all script tags
response.getVaadinResponse().setHeader("Content-Security-Policy",
"script-src 'nonce-" + nonce + "'");
// Add the nonce to all script tags in the host page
response.getDocument().getElementsByTag("script").attr("nonce", nonce);
Function Execution & Eval Calls
The example in the above section only adds the nonce to the application. It doesn’t help with handling all of the dynamic functions, eval
calls, and chunk loading. An application that needs to use strict CSP must provide a JavaScript file for handling these issues in a CSP-compliant way.
The Function constructor and eval
must be overridden. Also, the application developer needs to provide equivalent pre-defined JavaScript functions for any calls made by the application. A snippet of this is shown below as an example:
// Override the Function constructor with a version that uses inlined code if available
const originalFunction = window.Function;
window.Function = function(...args) {
const key = args.join(",");
if (functions.has(key)) {
return functions.get(key);
}
if (key.startsWith("return window.Vaadin.Flow.loadOnDemand(")) {
const chunk = key.split("return window.Vaadin.Flow.loadOnDemand('").pop().split("');").pop();
return function (chunk) {return window.Vaadin.Flow.loadOnDemand(chunk);};
}
// Expression was not found in functions map, and was not chunk-loading call, so log it to console
const code = args[args.length - 1];
// Ignore the stats gatherer which isn't used in production mode
if (code.indexOf("var StatisticsGatherer") == -1) {
const snippet = `functions.set("${key}",\n function(${args.slice(0, -1).join(',')}) {${code}});`
console.warn(snippet);
}
// Fall back to the original constructor.
// This will fail in strict CSP mode, but this way the browser will report an error.
return originalFunction.apply(null, args)
}
// Override eval(..) with a version that uses inlined code if available
const originalEval = window.eval;
window.eval = function(script) {
const originalArg = script;
// Removes parenthesis used by Vaadin Charts
if (script.length > 1 && script.substring(0, 1) === "(" && script.substring(script.length - 1) === ")") {
script = script.substring(1, script.length - 1);
}
if (evalCalls.has(script)) {
return evalCalls.get(script);
} else {
let snippet = "";
if (script.startsWith("function")) {
snippet = `evalCalls.set("${script}",\n ${script});`
} else {
snippet = `evalCalls.set("${script}",\n function() {return ${script}});`
}
console.warn(snippet);
// Fall back to the original eval.
// This will fail in strict CSP mode, but this way the browser will report an error.
originalEval.apply(null, originalArg);
}
}
In the example here, functions
and evalCalls
provide mapping from string to a predefined function. The next snippet shows a couple of examples only where a real application would have many more of these mappings:
const evalCalls = new Map();
evalCalls.set("'The value for <b>' + this.x + '</b> is <b>' + this.y + '</b>'",
function() {return 'The value for <b>' + this.x + '</b> is <b>' + this.y + '</b>'});
const functions = new Map();
functions.set("$0,$1,return $0.$connector.confirm($1)",
function($0,$1) {return $0.$connector.confirm($1)});
An example application using strict CSP is available on GitHub. Files containing the required changes are Application.java
and csp.js
.
B5E8EEC6-0F7E-49C5-8121-0906827BE4A5