Accessibility Now
Join our upcoming webinar about building accessible web applications! June 7, 2022.
Blog

The dangers of using the wrong abstraction for Vaadin access control

By  
Leif Åstrand
Leif Åstrand
·
On Jan 29, 2020 10:51:56 AM
·

Abstractions are important, but mismatched abstractions can be problematic. One particular mismatch that I encounter every now and then is a lot more than problematic: it can be outright dangerous. This is the mismatch between URL-based filtering in various security frameworks and view-based navigation in Vaadin.

If you only remember one thing from this post, let it be that access control for Vaadin views must be based on the actual Vaadin views, and not only on the URL. Keep reading to learn why that is, and what a safe solution looks like.

Security frameworks are designed to check request URLs

Almost anything you do in the browser that involves a server, sends an HTTP request to one URL or another. If you enter https://app.com/private-view in the browser's address bar, the browser sends a request to that URL and renders the HTML that it receives in the response. If the HTML contains something that shouldn't be publicly accessible, you can configure your server to handle requests to that URL in different ways, depending on whether the request contains proof that the user is logged in and has appropriate permissions. An unauthorized request can, for instance, receive a redirect to a login page, instead of the sensitive HTML.

Request and session cookie to the server, response from the server.

With a typical single-page application, the HTML document instead loads some JavaScript that bootstraps the client-side application logic and sends a request, for example to https://app.com/private-data, to load the sensitive data and then update the DOM to show that data. In this case, it isn't really necessary to prevent an unauthenticated user from loading the private-view URL, since the HTML or JavaScript doesn’t contain anything sensitive. Instead, it's the requests to the private-data URL that should be protected to prevent unauthorized access.

Generic web security frameworks, such as Spring Security, Shiro, or JAAS make it really easy to add protection on this level of abstraction, by configuring access-control requirements based on individual URLs or URL patterns. The problem is that Vaadin operates on a slightly different level of abstraction, and securing individual URLs may indeed appear to work, even though it actually doesn't work.

All internal Vaadin requests go to the same URL

The application developer doesn't have to think about requests and URLs thanks to Vaadin's abstractions. There are still internal requests to deliver events, such as button clicks, to the server. All these requests are sent to the same URL, regardless of the kind of event or the view from which it originates. To make anything related to Vaadin work, you must allow all requests to this URL. Once you have done this, access control based on URLs provided by your security framework doesn’t help you.

Private URL is blocked, public URL and Vaadin's internal handler are both allowed

It will initially appear to be working, since opening https://app.com/private-view will indeed be caught by the security framework, based on the URL, because the browser loads the initial HTML that bootstraps the application. The problem is that a malicious user can instead bootstrap Vaadin from some other URL that is allowed, for instance https://app.com/public-view or a login view. They can then run some JavaScript that makes Vaadin send an internal request to navigate to /private-view. This request is sent to the URL of Vaadin's generic handler, and not to the /private-view URL. The security framework that looks at request URLs has no chance of detecting this.

You can try this in your own application by opening the browser's JavaScript console, changing 'private-view' in the following snippet to the URL of a view in your application, and entering it into the console:

let link = document.createElement('a');
link.href = 'private-view';
link.setAttribute('router-link', '');
document.body.appendChild(link);
link.click();

This script creates an internal navigation link, adds the link to the DOM, and then emulates a click on that link. Vaadin has a global listener that intercepts click events from links with the router-link attribute. This listener sends an internal request that instructs the server to navigate to the application's @Route("private-view") class. It then instructs the browser to not perform the default action of requesting and rendering HTML based on the link URL. You can use the browser's network inspector to check that running this JavaScript snippet sends a POST request to /, but no request at all to /private-view.

Network inspector showing requests to /public-view and to /

In general, Vaadin only allows a user to perform actions if the UI component that triggers the action is visible to the user. However, this general principle does not hold true for navigation between views, because the user can navigate back and forth using the browser's back and forward buttons regardless of visible components. This also means that you need to apply access control for views based on the actual views, instead of only relying on URL pattern restrictions offered by a web security framework.

Check access for views instead of for URLs

To guard against this potential vulnerability, you need to block access based on the view that Vaadin is about to show, instead of doing so based on the URL to which the browser sends a request. You can, and should, still use your security framework for all other matters, such as logging in users, keeping track of the current user, and managing what permissions or roles each user has.

The most straightforward approach is to use callbacks in the view components. Add a check to the onAttach callback in every view and use the security framework to find or inject the current user and verify that they are authorized. You can make this slightly more sophisticated by using the callback from the BeforeEnterObserver or HasUrlParameter interfaces, instead of onAttach. In this way, you will get access to a BeforeEnterEvent that allows you to forward or reroute the user to another view.

While this approach is straightforward, it does have its drawbacks. The most obvious problem is that you have to remember to add the check separately to each view in your application: if you forget, the view will be publicly accessible. It can, in some cases, also be problematic that the view instance needs to be created before the access control check is done.

An alternative approach is to use a global BeforeEnterListener that is invoked every time navigation occurs. This listener can inspect the target view class to determine its access control restrictions based, for example, on a hardcoded lookup map or on annotations on the class. This approach allows you to report an error when encountering a view without any associated access control definition, instead of allowing everything by default.

At the time of writing, BeforeEnterListener still has the same potential limitation as manual checking does, in that the instances are created before you can check access control. This issue was recently fixed, but the improvement has not yet ended up in a release. Keep an eye on this ticket to find out when the change is released.

Button click passes the security framework but is rejected by a BeforeEnterListener

To add your BeforeEnterListener to each UI, you need to add a UIInitListener to VaadinService. This can be done with a VaadinServiceInitListener. If you're using Spring, you can simply add @Component to your listener implementation to make it a managed bean, since Vaadin's integration with Spring automatically uses such beans. If you're using CDI, you can use @Observes for ServiceInitEvent, since Vaadin's CDI integration fires it as a CDI event. Finally, if you use neither of these, you can create a META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener file and put the fully qualified class name of your listener implementation class as the file contents.

You can see a practical example of this in the Securing your app with Spring Security tutorial series. In particular, have a look at the Secure Router Navigation section that sets up the navigation listener. Even though that tutorial is focused on Spring Security, the same general concepts apply, regardless of the security and DI frameworks you use.

Leif Åstrand
Leif Åstrand
Leif Åstrand keeps an eye on the overall architecture of the Vaadin platform. He knows a thing or two about how Vaadin, Web Components, and the internet works.
Other posts by Leif Åstrand