Handling Authentication Secrets in the Browser

Posted by
on under

I gave a talk titled Handling Authentication Secrets in the Browser at Fluent 2017 in San Jose (you can see the slides above). As a complement to the talk, I thought it would be a good idea to write down the main concepts here on the blog as well, for those that weren't at my talk or those that were, but want to study the topic with more time than the 40 minutes I had for my presentation.

The Web Is Stateless

We tend to forget that the HTTP protocol is stateless. When working with protected resources, a client needs to send some form of identification along with a request, for example, a username and password that it obtained from the user. Once that request is complete, if the client needs to send a follow-up request, it will have to include identification again, since the server has no mechanism to "remember" the client from the previous request.

Related to this is the concept of logging in and out of web applications. At the HTTP protocol level, there is no such concept, this is implemented at a higher level through user sessions, and with the help of cookies or other HTTP headers.

Typical Forms of Authentication

The most common form of authentication is the username and password combination. A client application running in the browser asks the user for their credentials, and then forwards them to the server along with a request, typically in the Authorization header. Web browsers have built-in support for the Basic and Digest authentication mechanisms from the HTTP standard, but the user experience is extremely bad, so in most cases applications do not rely on the browser for this and implement their own handling of the credentials.

Since it is not a good idea to constantly send user credentials to the server, a common pattern for web applications is that they require this type of authentication only for the first request a client sends. When the identity of the client is verified, the server establishes a user session for the client, and then exchanges the credentials for a reference to this user session.

For modern rich-client type applications, the server will typically return a token, that the client needs to send back to the server in the Authorization header, or sometimes in a custom header that is specific to the application. For traditional thin-client web applications, it is very common for the server to return a reference to the user session in a cookie, so that the web browser automatically resends it back to the server with every request without the need to implement any application logic in the client.

Passwords, Tokens, Cookies... What's The Difference?

I've already mentioned the three most common forms of authentication in web applications, but how do they compare?

Passwords are in general created by humans, more specifically the users of the application. There are cases where servers assign randomly generated passwords to users when they open a new account, but those are considered highly insecure and are normally used to allow the user to login and immediately change the password to one of their own choosing. Tokens, on the other side, are always generated by the server.

It is common for tokens to have a scope associated with them, so for example, an administrator could have an option to request a regular user token while not performing administrative tasks, to avoid the risk of making changes by mistake. Passwords in general give full access to the user account, so losing a password could be much more devastating than losing a token.

Another area where passwords and tokens differ is in their validity. Passwords typically do not have expiration, or when they do, they last for a long time, typically several months. Tokens have shorter validity, and this reduces the risk of attacks if they are compromised.

Cookies are a general purpose storage mechanism that all web browsers support, so they don't really have much in common with passwords and tokens. However, I like to make a distinction between cookies in the general sense and authentication cookies, which are cookies that are specifically used for authentication. Cookies have the interesting property that the web browser sends them to the server automatically, without the application having to do anything, so they are many times used for authentication because of the convenience of having them handled by the browser.

User Sessions

I mentioned above that when a client sends a first request with "strong" authentication credentials such as username and password, the server exchanges those for a simpler form of identification which allows it to remember the client when it sends more requests in the future. This other identification mechanism that helps the server remember a client is the user session, which is a secure place where information about the authenticated client is stored.

A common place to store user sessions is a server controlled storage, as that is what makes it secure. This type of user sessions are typically stored in databases or disk files. In these cases, the server returns a session id to the client, which is an identifier that allows it to easily recall this user session whenever a new request from that client arrives.

Another style of user session is when the actual user session with all its data is sent to the client instead of just a reference to a server-side storage. This has the advantage that the server does not need to manage storage for the user sessions. To ensure that user sessions are not tampered with while in the client's possession, they are protected with a cryptographic signature that only the server knows how to generate. If the client attempts to modify the contents of the session, then the signature will not match the data anymore, and the server then rejects it. The JSON Web Tokens specification is a common solution for implementing signed user sessions.

From this discussion, we can conclude that a token can be defined as an identifier that maps to a server-side user session, or it can be the actual session, along with a cryptographic signature, using a JWT or similar format. When an application uses cookies for authentication, it also has the same two basic options in terms of what to store in the cookie, so we can say that authentication cookies store a token in them.

Basic Protection of Secrets

With the basics out of the way, let's now review the measures that need to be taken to protect user credentials in a web application.

The absolute most important thing that needs to be done by every web application, is to use HTTPS. This ensures that the traffic between the server and the clients is always encrypted, making it impossible for intermediaries to eavesdrop on the communication and obtain any data included in HTTP requests or responses. An SSL certificate also allows clients to verify that they are talking to the actual server they are trying to reach and not an impostor. Since these days we have free SSL certificates, there is really no excuse for not implementing HTTPS. I have written an article about HTTPS that discusses the steps to secure your server (note that parts of this article are specific to securing a Python web application that uses the Flask framework, but the final, part where Let's Encrypt SSL certificates are discussed, applies to any type of web application).

So let's say you've implemented HTTPS for your application, so now your secrets are protected while in transit between client and server. The next thing you need to consider, is protecting your server to ensure attackers cannot steal secrets there. There are a number of relatively easy configuration changes that you should make:

  • Use strong passwords for all SSH accounts, or better yet, only allow logins with SSH keys.
  • Disable root logins
  • Make sure that all cookies that store authentication have the secure flag set, to prevent them from being sent over regular HTTP connections.
  • Make sure only the services that need to be accessed publicly listen on public network ports. Typically, your database does not need to be accessible from outside your server or private network of servers, for example.
  • Add a firewall that only exposes the necessary ports to the world, which may end up being just ports 80 and 443 for HTTP and HTTPS and port 22 for SSH.
  • If your application crashes, make sure developer error information such as a stack trace is logged internally, and never returned in the HTTP error response.
  • Keep your server up to date with security patches.

Web Browser Specific Attacks

Unfortunately, the measures from the previous section are not going to be enough to protect you against attacks. Web browsers are strange in that they hold session information for lots of different applications, and attackers are always looking for holes in the browser security model that allow them to access information from all these applications the browser knows about.

There are two very common vectors of attack specific to web browsers that occur because applications fail to protect secrets in the browser:

  • Cross-Site Scripting, or XSS
  • Cross-Site Request Forgery, or CSRF (or sometimes XSRF)

Besides having somewhat similarly cryptic names, these two are really completely different, and have different protection strategies.

Cross-Site Scripting Attacks

XSS is a type of code injection vulnerability that puts sites that have social features at risk. In particular, this type of attack can happen when users are allowed to write comments or posts that other users can see.

The attack involves a malicious user writing a comment that includes embedded JavaScript code in it. If the application does not validate for this condition and accepts the comment as given by the attacker, when other users view that comment in their browsers, the JavaScript that was embedded will actually execute as if it came from your application. In fact, it is your application that is providing that JavaScript code, so it will run fully within its context. The malicious JavaScript code will be able to read cookies, local storage or DOM contents and upload what it gathers to an attacker controlled server, without the user even noticing.

In the Fluent talk, I used a picture from the Alien parody Spaceballs to represent this type of vulnerability in a humorous way, since the JavaScript code injected by the attacker becomes a parasite that lives and feeds off of your own application.

XSS

How to Protect Against XSS Attacks

Keep in mind that if your application does not allow users to contribute content that other users can see, then your risk of XSS attacks is much lower, attackers would have to find a way to make content they provide visible to other users, in an application that does not provide that level of sharing.

Now if your application does have social features, the most basic thing you should do to prevent XSS attacks is to routinely audit your code and make sure all input from users is sanitized, in particular with regards to comments or other social content users contribute. But because we are humans and we often tend to make mistakes, relying on audits alone is not enough. Luckily, there are a number of measures you can take to protect yourself against this nasty type of attack:

  • Use authentication cookies, and make sure they have the httpOnly flag set. This is a fantastic protection against XSS attacks, since cookies that are httpOnly cannot be read from JavaScript, making them virtually invisible. Of course this means your application will also not be able to access the cookies, which is usually not a problem because the browser will send them to the server on its own.
  • Using as short an expiration as possible in your tokens is a good way to make sure that if a token is compromised, at least the window of time in which it can be exploited is not large. Obviously expiration times will have to be set according to the type of application you have.

A practice that I've sadly seen too many times involves the use of third party access tokens or credentials in client-side applications. As an example, this includes OAuth tokens from Facebook, Google, etc. and also access keys from AWS accounts. The problem with this practice is that if an attacker manages to successfully inject JavaScript code into your application, there is absolutely no way to protect these secrets that exist in the context of the client-side application from being stolen. So this is a terrible idea, never ever let an application designed to work in a browser handle third party secrets. These secrets should be kept safely stored in the server, and if the client application needs to use them, it should do so by using your application server as an intermediary.

Cross-Site Request Forgery Attacks

CSRF attacks are remote attacks, in the sense that you don't need to have your application infiltrated with the attacker's code to be vulnerable like in the XSS case.

With CSRF, the attacker owns a malicious site that sends XHR requests to your server. If your application uses authentication cookies, there is potential risk when a user visits your site and logs in, and then is lured to the attacker's site. If the JavaScript code in the attacker's site sends a request to your application server, the browser will happily attach your application's authentication cookies to the request, without considering that the user is now on a different site. The attacker's JavaScript will not be able to read the cookies, but the browser will send them anyway. So from your application server, you are getting a request that looks valid in every way, since it includes the original user session that your application stored in cookies.

In the talk, I used a picture from the movie Face/Off to illustrate this type of attack. In the picture you can see John Travolta, which is the good guy. He will be able to go anywhere John Travolta is allowed to go, because he has John Travolta's face. But in reality, this is the bad guy, played by Nicholas Cage until he receives a face transplant. Setting aside the silliness of the movie's premise, with CSRF, the "bad" requests that the attacker sends, are disguised as "good" requests with the help of the browser's cookie policy.

XSS

How to Protect Against CSRF Attacks

The techniques to protect against CSRF are completely different, and even contradictory at times, to those we use for XSS. The basic level of protection for this attack vector is for your application to check the Origin and/or Referer headers sent by web browsers, which indicate what is the originating site making the request. These headers cannot be set by JavaScript, so the attacker has no way to forge them. Unfortunately only modern browsers implement these headers, so this does not give you foolproof protection against this type of attack.

The more involved measure that you can take against CSRF attacks is to implement a so called synchronizer token. The basic idea is that the server comes up with a random sequence of characters, which we will call a CSRF token, different from the token used for authentication purposes. The server keeps track of the CSRF token assigned to each client, and then expects the client to send it back with any follow-up requests. If the token does not match what the server has stored, then the request is discarded.

There are many possible implementations of this idea, but one that I like a lot involves the use of a cookie. The server writes the CSRF token to a cookie that does not have the httpOnly flag, giving your client-side application full access to it. The client then reads the token from the cookie, and sends it back in all form posts and XHR requests, either in a custom header or a hidden form field, so basically the client will be sending the CSRF token twice, once in the cookie and once in another way. The server then needs to read the cookie and other value and ensure they are equal. An attacker's site will not be able to read the CSRF cookie, so it has no possibility of ever sending a request that looks authentic.

Another alternative that can be used instead of a synchronizer token, is to give the authentication token a second function as a CSRF token. If your application sends the token to the client in a Authorization or custom header, this is enough protection against CSRF. If the token is stored in a cookie you are not protected though, because the browser will send the cookie from the attacker's site, as discussed above. An attacker's site will not be able to read your token... unless the token is compromised in a separate XSS attack! Recall that the best protection against XSS is to use authentication cookies, but for CSRF cookies can be a problem, so it is tricky to find the right security model.

Dealing With Compromised Credentials

After this excruciatingly long discussion of all the attack vectors and how they can be mitigated, I have really bad news to give you, and that is that your application is still vulnerable to attacks.

Unfortunately we live in an imperfect world. We developers make mistakes, that attackers are very good at exploiting. Users also make mistakes, or are careless in how they manage their security. So really there is only so much you can do to protect the credentials of a user, when that user uses the same password on many other sites, and some of those sites don't have the level of security you have on your own. If an attacker gets hold of a user's password on one site, it is really obvious that this password is going to be tested against other sites, so eventually, you will find that attackers can infiltrate your application, and there is really not much you can do about it.

So basically, my last piece of advice comes down to having a plan of action for when a vulnerability occurs. What does it mean to have a plan? I can list the following items for you to consider:

  • If based on a user's complain you determine that a token is being used by an attacker, then you need to have a way to revoke that token immediately. I see many times this is not given a second thought, so then when a token is compromised there is no way to revoke it, or when there is, it involves someone hand editing database records, which is error prone, specially when you have to do it under pressure.
  • If a user reports their account password was compromised, you need to have a way to reset the account so that the user can recover control. This means being able to revoke all active tokens for that user, ensuring the contact information has not been altered, and finally resetting the password on the account.
  • If you think your own server might have been compromised, then you need to revoke your SSL certificate and get a new one, change all your account, database and other passwords, change your token signing key (which invalidates all tokens for all users), and reset all user accounts.
  • You should have tests in place for the three items above, so that if bad luck strikes, you know you have a process in place you can rely on.

Conclusion

I hope you found this article useful in improving the security of your web application. Do you have any questions that are not covered in the article? Let me know below in the comments.

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

4 comments
  • #1 Androiddrew said

    Ok, so if a client has a JWT that is sent through the Authorization header then the user session it represents protects against CSRF. Why is this the case? Wouldn't we still be under the same danger of a copy cat site forwarding XHR requests to our backend?

  • #2 Miguel Grinberg said

    @Androiddrew: If the copy cat site manages to get hold of a valid JWT token, then yes. But how are they going to get it? With a cookie, they don't need to do anything, because the browser sends the cookie for them when they send their malicious XHR request.

  • #3 Daniel said

    Hi Miguel, thanks for the detailed info! A few questions:

    "The client then reads the token from the cookie, and sends it back in all form posts and XHR requests, either in a custom header or a hidden form field, so basically the client will be sending the CSRF token twice, once in the cookie and once in another way. The server then needs to read the cookie and other value and ensure they are equal. An attacker's site will not be able to read the CSRF cookie, so it has no possibility of ever sending a request that looks authentic."

    Why can the attacker's site not read the CSRF cookie? Is it because even with non-HTTP cookies, you can only read cookies that are assigned to the current domain?

    Also, that CSRF cookie will still be sent in requests from the attackers site, so I assume on your API you are looking for the CSRF cookie in a custom header, NOT in the passed cookies right?

    Cheers :)

  • #4 Miguel Grinberg said

    @Daniel:The browser's security model does not allow site X.com to read the cookies that were set by Y.com. The CSRF token is normally expected in a cookie and in some other form, like a custom header, a form field or query string. When the request comes from the attacker's site it will only be the cookie, since the attacker has no way to read the cookie to copy the token to the 2nd place.

Leave a Comment