My standard answer related to mixing Vaadin Flow and CSP has been that it's not feasible because of architectural limitations. I was wrong. The architecture just needs a little bending, and suddenly CSP without unsafe-eval
can be used with Flow.
Content Security Policy (CSP) is a browser standard that among other things lets the application define rules for how JavaScript is loaded and run. This gives an additional layer of defence against many types of cross-site scripting attacks since the browser would refuse to run any scripts that aren't separately defined as trusted.
Flow makes it possible to integrate arbitrary web components by mapping actions through the Java API into corresponding JavaScript invocations that are carried out in the browser. A well designed web component can achieve quite much only based on the DOM structure with child elements and attributes, but there's a limit for how far that can go especially in more dynamic cases such as lazy loading data into a grid. In combination with CSP, this means that the unsafe-eval
option needs to be enabled which in turn removes most of the benefits of CSP.
An example of the challenge
Let's say we want to use a web component that renders a button, but with the twist that it doesn't fire a regular DOM event when it is clicked but instead expects the user to assign a click listener to a handleClick
property on the web component. For a client-side developer, using this component could be expressed like this:
render() {
return html`<my-button .handleClick=${() => {
window.alert("Well done")
}}>Click me</my-button>`;
}
Something similar in a Java MyButton component class in Flow could be done with a short JavaScript snippet to create a callback and assign it directly to the element property.
public void setClickGreeting(String greeting) {
getElement().executeJs(
"this.handleClick = () => window.alert($0)",
greeting);
}
Running that Java code causes (slightly simplified) a message containing the JavaScript expression and the greeting value to be sent to Flow's client engine which runs code along these lines to first evaluate the JavaScript expression into a new function and then invoke that function with the target element bound do this
and the greeting bound to the first function argument that is named $0
.
let f = new Function("$0", "this.handleClick = () => window.alert($0)");
f.call(element, "Well done");
The Function
constructor allows evaluating arbitrary JavaScript expressions and is therefore blocked when CSP is used unless the unsafe-eval
option is enabled.
A prototype to deal with the challenge
It turns out that it is still possible to enable CSP with Flow's architecture for web component integration thanks to the way those JavaScript expressions from the server are almost always hardcoded strings. I started by building a prototype to show that this is practical, and then I also imagined some new functionality that could be added to Flow to make it feasible to use the same approach in real applications.
Since the same JavaScript expressions from the server will be evaluated every time the application is run, we can directly add the corresponding code as regular JavaScript inside the application's frontend bundle. We then need to intercept calls to the Function constructor and return an existing function instance instead of evaluating the expression to create a new one. Thanks to the dynamic nature of JavaScript, we can intercept calls to new Function
simply by writing our own implementation to window.Function
.
If the example above would be the only case in the application that evaluates JavaScript from the server, then this code in the client-side bundle would do the trick.
function alertFunction($0) {
this.handleClick = () => window.alert($0);
}
window.Function = (...args) => {
if (args.length == 2 && args[0] == "$0"
&& args[1] == "this.handleClick = () => window.alert($0)") {
return alertFunction;
} else {
console.error("Unsupported expression", args);
}
};
It turns out that a simple application from start.vaadin.com uses 15 different JavaScript expressions even without trying to integrate my imagined web component. This just means that the collection of hardcoded functions may grow quite large and I ended up adding slightly more structure to my actual prototype script than the simplification that I showed here.
Aside from that, enabling CSP is only a matter of defining a BootstrapListener that generates a random nonce for every bootstrap page, updates all the script tags to use that nonce and finally sets a Content-Security-Policy
header that defines that JavaScript should be loaded only for script tags that include that nonce value. The header can be set only in production mode since webpack in development mode is internally using eval()
to load scripts.
The sources for this prototype are on GitHub. The prototype application is also deployed even though the only interesting thing in the application is that it works even though there's a Content-Security-Policy
header in the HTML page response.
A potential new Flow feature
With a prototype that shows the feasibility of assuming hardcoded script expressions, I started to ponder how to make it more practical to build an application that uses CSP. My conclusion is that there would have to be a mechanism in Flow which automatically finds the script snippets that might be used and includes them in the frontend bundle. This in turn means that new functionality would have to be introduced to replace all current use of executeJs
and other similar APIs.
This new API would have to balance between two constraints: script expressions must be findable at compile time while still making it easy to trigger invocations from Java code. Findable at compile time leads to use of Java annotations while triggering can be done based on a Java Proxy instance that implements the annotated interface.
Going back to the imagined button component, the annotated interface could look like this:
public interface MyButtonScripts {
@JsExpression("this.handleClick = () => window.alert($0);")
void setHandler(String greeting);
}
This design has the added benefit that method signature in Java describes that the parameter type expected by the script is a string. To use this interface, the component implementation would request an instance of the interface from the framework and then invoke that method.
public void setClickGreeting(String greeting) {
getElement().getJsInvoker(MyButtonScripts.class).setHandler(greeting);
}
Another benefit of this design is that it opens the door for moving the expressions to a separate JavaScript module file and reference its exported functions from Java. The interface definition would then just have to reference a script file from e.g. /frontend
using the same path structure as the existing @JsModule
annotation.
@JsExpressionModule("./myButtonScripts")
public interface MyButtonScripts {
void setHandler(String greeting);
}
Usage from the component would be the same as with an inline implementation, but we would now also need a frontend/myButtonScripts.js
file that contains the implementation.
export function setHandler(greeting) {
this.handleClick = () => window.alert($0);
}
These ideas are also documented in #10759. There's also a supplementing issue, #10810 that describes a transition mechanism that applications could use to manually intercept cases that still use the old executeJs
API without having to resort to redefining window.Function
.
A plea for input
These ideas for supplementing Flow is currently only a wild idea. We need your feedback to understand how important this would be for you and whether the imagined solution approach would make sense for you. You can leave a comment below or reach out to us in various other sensible ways.