Content Security Policy
- Required Settings
- Vaadin & Strict CSP
- Creating Random Nonce
- Function Execution & Eval Calls
- Checking Compatibility
A Content Security Policy (CSP) is a browser standard that helps to mitigate certain types of attacks, such as Cross-Site Scripting (XSS) and data injection. With a CSP, the application defines rules for how scripts and other resources can be loaded and run in the browser. This page describes the settings that Vaadin Flow needs, and how to use a strict, nonce-based policy.
Required Settings
Vaadin Flow needs the settings script-src 'unsafe-inline' 'unsafe-eval' and style-src 'unsafe-inline'. The bootstrap page starts the Flow client engine — the part of the framework that runs in the browser — with an inline script. The engine then runs the JavaScript expressions that the server sends, such as those of executeJs() and of event data expressions, by constructing functions from them, which is what unsafe-eval permits.
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
The application generates a new nonce for each request of the bootstrap page in an IndexHtmlRequestListener. The listener sends the nonce in the Content-Security-Policy header, and adds it to the scripts and module preload links of the page. The csp-nonce meta tag provides the nonce to the module preload links that the page adds later, when it loads more of the application. The following example registers the listener in a Spring application:
Source code
Java
@Bean
VaadinServiceInitListener cspNonceInjector() {
return event -> event.addIndexHtmlRequestListener(response -> {
if (!response.getVaadinRequest().getService()
.getDeploymentConfiguration().isProductionMode()) {
return; // Strict CSP works only in production mode
}
String nonce = UUID.randomUUID().toString();
// Make the browser require the nonce for all scripts
response.getVaadinResponse().setHeader("Content-Security-Policy",
"script-src 'nonce-" + nonce + "'");
// Add the nonce to the scripts and module preloads of the page
Document document = response.getDocument();
document.select("script, link[rel=modulepreload]")
.attr("nonce", nonce);
document.head().appendElement("meta")
.attr("property", "csp-nonce").attr("nonce", nonce);
});
}Function Execution & Eval Calls
The nonce alone doesn’t cover the JavaScript that the server sends as strings. The Flow client engine runs it by constructing functions with the Function constructor, which a strict policy blocks. An application that uses strict CSP must therefore override the Function constructor and eval() with versions that return pre-defined functions instead. Put the overrides into a JavaScript file, such as src/main/frontend/csp.js, and load it with @JsModule("./csp.js") on the main layout of the application:
Source code
JavaScript
// Override the Function constructor with a version that uses pre-defined functions
const originalFunction = window.Function;
window.Function = function(...args) {
const key = args.join(",");
if (functions.has(key)) {
return functions.get(key);
}
// Calls that load the frontend chunk of a view
const chunkCall = key.match(/^return window\.Vaadin\.Flow\.loadOnDemand\('([^']*)'\);$/);
if (chunkCall) {
const chunk = chunkCall[1];
return function() { return window.Vaadin.Flow.loadOnDemand(chunk); };
}
// Log the code to add to the functions map
const code = args[args.length - 1];
console.warn(`functions.set(${JSON.stringify(key)},\n function(${args.slice(0, -1).join(',')}) {${code}\n});`);
// Fall back to the original constructor.
// This fails in strict CSP mode, but this way the browser reports an error.
return originalFunction.apply(null, args);
};
// Override eval() with a version that uses pre-defined functions
const originalEval = window.eval;
window.eval = function(script) {
const originalArg = script;
// Removes parentheses used by Vaadin Charts
if (script.length > 1 && script.startsWith("(") && script.endsWith(")")) {
script = script.substring(1, script.length - 1);
}
if (evalCalls.has(script)) {
return evalCalls.get(script);
}
// Log the code to add to the evalCalls map
if (script.startsWith("function")) {
console.warn(`evalCalls.set(${JSON.stringify(script)},\n ${script}\n);`);
} else {
console.warn(`evalCalls.set(${JSON.stringify(script)},\n function() {return ${script}\n});`);
}
// Fall back to the original eval().
// This fails in strict CSP mode, but this way the browser reports an error.
return originalEval(originalArg);
};The functions and evalCalls maps map the code that the server sends to pre-defined functions. Which entries an application needs depends on the components and features that it uses, and on the Vaadin version. To collect them, run the application in production mode without the Content-Security-Policy header, and use every view and feature. For every function that’s missing from the maps, the overrides log a line to the browser console that you can copy into the file. Collect the entries again after upgrading Vaadin.
The following snippet shows a few entries. A real application has many more:
Source code
JavaScript
const functions = new Map();
functions.set("event,element,return (event.shiftKey)",
function(event,element) {return (event.shiftKey)});
functions.set("$0,if ($0) window.Vaadin.Flow.gridConnector.initLazy($0)",
function($0) {if ($0) window.Vaadin.Flow.gridConnector.initLazy($0)});
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>'});Checking Compatibility
The csp configuration parameter prepares applications for a strict, nonce-based policy without these keywords in script-src. Set it to warn to log every call in the application that wouldn’t work under such a policy. The strict mode can’t be used for applications yet, because Vaadin itself still uses APIs that need eval, for example for the event data of click listeners. See Configuration Properties for the parameter.
B5E8EEC6-0F7E-49C5-8121-0906827BE4A5