SPAs (Single Page Applications), which make extensive use of Javascript, often need to call Application Programming Interfaces (APIs) to offer services to the user. Therefore, these applications must get access to those APIs, i.e. get authorized. This can be achieved in several ways. Modern standards such as OAuth2 have been defining different flows depicting how authenticated users (or applications acting on their behalf) should receive tokens or credentials that can then be used when calling the APIs to prove they are authorized to call them.
This means SPAs acting on behalf of those users have to somehow store those tokens, be it access, refresh or id tokens. Places like Javascript local storage or session storage are frequently suggested.
Attacks
XSS
However, these locations are vulnerable to XSS (Cross-Site Scripting) attacks.
XSS generally boils down to the injection of malicious javascript code, e.g. in one of the SPA’s dependencies. When executed by the user’s browser, the SPA will run the injected code which may try to read the browser local and session storage content and send it to one of the hacker’s remote databases. If tokens/credentials are saved in those locations, security is compromised as the hacker is now able to impersonate the user for as long as the tokens/credentials are valid or refreshable.
XSS’s nemeses are HttpOnly Secure Cookies. By wrapping a token/credential in one of those, Javascript will not be able to access their content anymore and the browser will only pass back those cookies with calls to the domain/address that did set them up.
CSRF
However, wrapping tokens/credentials in cookies open up a vulnerability to CSRF (Cross-Site Request Forgery) attacks. What that new beast is capable of is best explained with the following very popular scenario:
“On one tab of his browser, a user is logged in on his bank account SPA which returned an HttpOnly Secure Cookie to the browser. On every request the user makes to his bank, the cookie will be passed by the browser on to the API backing the bank SPA to prove he is authorized to make those calls.
In the meantime, the user opens up another tab into a malicious website inviting him to click on a video of a kitten stuck in a fridge. By clicking on the video, the malevolent website will send a request to the user’s bank, asking to transfer all his savings to the hacker’s account. The browser, seeing that the destination of the request corresponds to the HttpOnly Secure cookie issued to the user when he logged in to the bank, will happily pass the cookie. The bank API will unwrap the cookie, notice the token/credentials are legit and execute the money transfer.
The hacker successfully committed her mischief and can now enjoy an early retirement plan.”
To make CSRF attacks null and void, the usual solution is to make the SPA pass the tokens/credentials (i.e. using Javascript) in the header or other accepted location of the requests to the APIs.
The Dilemma
So, in a nutshell, we face the following dilemma:
Tokens/credentials wrapped in HttpOnly Secure Cookies are immune to XSS but vulnerable to CSRF
Tokens/credentials handled by Javascript and passed to requests headers or the like are immune to CSRF but vulnerable to XSS
The Best of Both Worlds
To be resistant to both XSS and CSRF attacks, the most common mitigation strategy roughly follows those lines:
Add a header with a randomly generated string (often called CSRF token, or state in OAuth2 terms)
Add that same randomly generated string to the token/credentials (most easily achieved as an additional claim when using JWT, i.e. Json Web Tokens)
Wrap the tokens/credentials in a HttpOnly Secure Cookie
Ensure the API verifies the randomly generated string contained in both the header and the cookie do match
→ XSS will not work because of the HttpOnly Cookie
→ CSRF will not work because the attacker does not know which random string to send in the header together with the HttpOnly Secure Cookie
Remaining Problems
Involved hacking
One can still face the scenario of an attacker getting the CSRF token through an XSS attack, and passing it to a hidden request on the attacker’s malicious website, forcing the browser to pass the HttpOnly Secure Cookie to the request.
This scenario, albeit possible, is much less likely to arise in practice, as this attack is a much more specifically targeted and thoroughly engineered one.
OAuth2 Implicit flow
Perhaps of bigger concern is the implicit flow defined by the OAuth2 standard.
This flow specifically targets SPAs, and mandates the authorization servers implementers to return tokens directly to the SPAs, hence making them vulnerable to XSS attacks.
This pattern has been praised for several years now, and only in 2018 has this flow started to be revised to tackle that vulnerability.
The problem is that most businesses do not have the time nor the resources to build and maintain authentication and authorization servers. Luckily, to the rescue come the AaaS (Authentication as a service) providers such as Auth0, AWS, Google and so forth, which businesses have heavily relied on these last years.
Unfortunately, these providers are not up to date with the latest reforms of the OAuth standard and they do not necessarily strictly follow them. And, AFAIK, this flow has not been revised by the main AaaS implementers in mid 2019. Moreover, AaaS implementers control how tokens are being sent to the applications. Therefore, businesses relying on AaaS cannot change the flow to return both CSRF tokens and token wrapped HttpOnly Secure Cookies.
For a more involved dig in the OAuth2 implicit flow plague, I warmly encourage keen readers to go through the following post: https://auth0.com/blog/oauth2-implicit-grant-and-spa/ of Vittorio Bertocci, principal architect of one of the main AaaS providers. The post extensively goes through the OAuth2 journey and demonstrates the current lack of effective solution to securely use the implicit flow today.