The CVE that wasn’t to be
As security researchers we take pride in helping our clients by identifying security issues before they wreak havoc. We usually focus on code developed by our clients but sometimes we broaden our efforts to include commonly used software such as webbrowsers. We believe fame (not fortune) awaits us in the form of an assigned CVE if we’re able to identify an issue in, say, the WebStorage implementation of Firefox.
LocalStorage and SessionStorage (commonly know as the WebStorage API) are modern alternatives to the most ubiquitous data-persistence technique on the web: cookies. Cookies still dominate the realm of data-persistence in browsers, especially for session tracking. However, WebStorage features a more powerful alternative to cookies for storing data and is gaining momentum. However, as we will see, there is inconsistency in the WebStorage implementations of browsers, which can lead to interesting edge-cases.
In our assessments we see a large variety common functionality implementations. Every framework, project, client, developer has their own favorite way of maintaining sessions and persisting user-data. It is our task to understand these implementations and look for ways to break assumptions of developers and determine how the application handles ‘weird stuff’.
The project in this case is a modern Angular single-page application which uses a REST-based API. It performs user authentication, authorization, and session management using the ‘Authorization’ HTTP header of each XHR request. Upon a successfull login the server responds with a bearer token, which is stored in the browser’s LocalStorage. The Angular client includes the token in the ‘Authorization’ header for all authenticated requests. The application performs log-out requests by emptying LocalStorage (removing the bearer token) and redirecting the user to the log-in form. So far so good.
As long as the application does not have XSS vulnerabilities and the token is sufficiently unpredictable, we can feel pretty confident about the safety of our authentication mechanism, right?
Now imagine the following scenario:
- A user logs in to the application.
- Our user opens a new tab, and starts multitasking in the application. So far, everything is working fine. LocalStorage is shared, which results in the second tab using the bearer token to communicate with the server. Our user didn’t have to log in a second time. Great.
- Our user now decides to leave the application and visit a different website in one of the open tabs.
- Our user explicitly logs out from the application in the other tab (erasing LocalStorage) and closes the tab.
- Let’s say our user realizes they forgot to perform some important task, and decides to navigate back to the application in the open tab by using the ‘back’ button.
What should happen?
The user is now presented with the log in-form or an informative message: ‘you are now logged out’. In any case,the user should not be authenticated anymore since the bearer token has been removed from LocalStorage. However, our user was STILL authenticated and the bearer token appeared to be resurrected: it was visible in the local storage again!
Why did this happen?
We use Firefox and started digging in the source code and conducted several tests. It turned out that Firefox has a peculiar way of syncing LocalStorage events between browser windows.
The following screenshot neatly illustrates the issue:
When Firefox handles a LocalStorage.clear() event, it ignores all tabs that are not an active window or ‘frozen’. Since Firefox maintains a LocalStorage memory cache for each tab (since persisted LocalStorage read/writes are I/O expensive), we now have two entirely separate states for our application. One which is still logged in by virtue of the cached LocalStorage, and one logged out by virtue of an up-to-date LocalStorage.
Creating a new tab by duplicating from the logged in tab results in a unauthenticated session, since the newly created tab will create a new cache from the up-to-date LocalStorage which is empty of course.
According to the bugfiling (https://bugzilla.mozilla.org/show_bug.cgi?id=1530944) this issue is going to be fixed when the next generation implementation of LocalStorage is enabled by default (not yet the case with Firefox 68), however even with the flag `dom.localstorage.next_gen` enabled we were still able to reproduce the issue. When testing our findings with the Nightly version of Firefox (71), we could not reproduce the issue anymore.
So far a pretty doom and gloom picture has been painted. However, there is a simple technique that allows you to continue to use LocalStorage for Auth purposes without being exposed to the issue described above.
In our case the LocalStorage is cleared, but no log-out event is sent to the server, meaning the server doesn’t even know the user has logged out. By having the Angular front-end send a request to the API so that the Auth token can be invalidated is an easy way to prevent the token from being used after log out although it does introduce state (the application needs to blacklist valid bearer tokens).
Ideally you should be able to rely on the specification of a given technology to perform the same across different implementations. The W3C spec for the WebStorage API states that a storage notification is to be sent for every method that is called on a Storage object (https://www.w3.org/TR/webstorage/#the-localstorage-attribute). While this is the case in Firefox the *handling* of this storage event leaves something to be desired.
Upon discovery of this weird behaviour in Firefox we were pretty excited to see whether we were the first to discover it. However, it turned out be to be a known issue which is to be fixed with the release of the new WebStorage implementation. No pentester-street-cred for us this time. 🙂