0

If I were to implement a common OpenID Connection pattern on a SPA, I might have the following relationship:

Auth server <-----------> Client (browser) <-----------> App API server

The user would be redirected to the auth server to login, and an HTTP-Only cookie is set on the auth server with the user's ID token (whose payload contains user details and is signed by a secret) and auth token. Then the auth server redirects the user back to the app with the tokens in the hash string, so they can be persisted.

The hash would have to be set by having the API return something like:

res.send(`<script>
  location.replace('${callbackUrl}#access_token=${accessToken}&id_token=${idToken}')
</script>`)

rather than a HTTP redirect with the token in the query string (because that would probably be logged), and the client-side app can use history.replaceState() to wipe the tokens from browser history. The browser strips these tokens from the hash so the user won't accidentally bookmark or share this URL containing the tokens, then it persists the tokens to localStorage.

I gathered from this article on Stormpath that localStorage isn't the greatest of ideas, since it's vulnerable to XSS (any arbitrarily executed JS living on the same page can read the token from localStorage).

So they advocate for using HTTP-Only cookies to store tokens instead. In this approach, the auth API redirects the user back to the app with headers to set HTTP-Only cookies with:

  • access_token
  • id_token
  • csrf_token

These cookies are set on the auth API server. The UI would also need to know the value of the csrf_token. If the authentication happened with an AJAX request, we could send it in a header; in the case of OpenID, where the app redirected to the API domain, we'd have to use the script approach and send it in the hash for the app UI to receive it. As I understand it, these approaches are roughly equivalent in terms of security -- whether read from the hash or from a header, either way the browser must store this CSRF token to send it in subsequent requests.

Angular's HTTP service handles the former approach (header) by grabbing the token and saving it to a cookie on the UI domain. Then it automatically sends requests to the auth API with the X-XSRF-Token set to that value it has stored in the cookie. Then the API can confirm that this given token matches the one in its csrf_token cookie and determine if the user submitted this request.

This is considerably more complicated than the localStorage approach. The article proposes that this is more secure because localStorage is vulnerable to XSS -- an attacker embedding malicious JS on a page on the UI's domain can steal the localStorage values. (Obviously XSS can be mitigated, but there are certainly situations that arise where we could be vulnerable to XSS, such as a CDN the UI uses getting hacked and sending malicious scripts, or simply a npm package that appears benign and is used in the UI but secretly captures localStorage in the background.)

But how is this CSRF approach any more secure? After all, the CSRF token must be stored in a cookie on the UI, and it can't be a HTTP-only cookie, since requests to the API must include the token in a header. So wouldn't this mean the same XSS vulnerability? The malicious script could read the CSRF cookie on the UI and start sending requests to the API using it.

Am I missing something here? How is the CSRF cookie more secure than putting a token in localStorage, if in either case, the primary means of authenticating with the auth API can be read from the client?

M Miller
  • 153
  • 1
  • 4
  • The only solution I can think of is for the ui server to have one non-UI endpoint that resides on the same domain. Auth API sends POST to `ui.example.com/authorized` with the CSRF in the payload as well as some shared secret. `/authorized` endpoint must verify the secret, set a HTTP-only cookie, then redirect to the UI home page. Then, sending AJAX requests `withCredentials` would send the HTTP-only cookie. Doing this, though, would require the UI to run a server instead of, say, just being hosted on S3 or other static provider. – M Miller Nov 30 '17 at 18:57
  • In general the xsrf token is in addition to any authorization. – ste-fu Nov 30 '17 at 19:04

1 Answers1

1

As per the OWASP cheat sheet https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet None of the prevention techniques are effective if you have a XSS vulnerability in your site.

ste-fu
  • 1,092
  • 6
  • 9
  • Would that literally mean that there is zero purpose to implementing the complexities of CSRF over just using localStorage? – M Miller Nov 30 '17 at 20:06
  • 1
    @MMiller Not at all - CSRF is still a risk to mitigate when you don't have XSS vulnerabiltiies in your site – ste-fu Dec 01 '17 at 10:31
  • @MMiller Also I **think** that localStorage would be vulnerable to a generic script that could just exfiltrate all the data including your access token and crsf token, but the http only cookie would require more effort from a would-be attacker – ste-fu Dec 01 '17 at 10:41
  • From my understanding, CSRF is a risk that only applies to sending requests that use cookie authentication -- so if your app were to only authenticate requests using Authorization headers and tokens coming from localStorage, deciding instead to use HTTP-only cookies and forgo localStorage won't really help, right? Because LS is only vulnerable to XSS and not CSRF, and HTTP-only cookies are only vulnerable to CSRF and not XSS. – M Miller Dec 03 '17 at 17:59
  • If your app does graceful degradation with, for example, JS-disabled support, you'd need to use cookies for authentication and hence need to implement CSRF; but if you only send requests using AJAX and a token stored in LS, it's really not any better to use cookies, right? Because you'll still be equally vulnerable to the only thing you were vulnerable to with LS, which is XSS. I believe the Stormpath article is wrong in saying it's less secure to store tokens in LS than it is to use HTTP-only cookies. – M Miller Dec 03 '17 at 18:03
  • To your point, it'd be possible to *read* the token from LS, versus impossible to read its value from a cookie; but, in either case, you could use XSS to not only make requests on behalf of the logged-in user, but also to read the responses (unlike CSRF attacks). So while you couldn't actually extract the value and use it yourself, you can accomplish virtually the same kind of attacks while leveraging XSS. I guess maybe with XSS you couldn't, say, bypass a CAPTCHA within the app, whereas you could if you could steal the token and log into the app yourself. – M Miller Dec 03 '17 at 18:06
  • I referenced this conversation in a comment on https://stackoverflow.com/questions/36980058/do-cookies-protect-tokens-against-xss-attacks/36980059#36980059 . Also linked to this question on https://security.stackexchange.com/questions/177300/what-happens-if-my-anti-csrf-token-is-compromised-by-an-xss-attack – Daniel Jan 15 '18 at 00:42