Here’s the scenario – a user logs in to your website, comes back tomorrow and… has to log in again. The idea of the “remember me” feature – and let’s face it, we’ve all seen this before – is that their authenticated state is persisted beyond the immediate scope of use. What this means is that they can close the browser, turn off the PC then come back tomorrow or next week or next month or however much later you determine is a reasonable timeframe and the site still knows who they are and offers them all the same features they had when they left it.
I’m talking about this little guy here:
Seems easy, right? It can be, but as you’ll see it’s also not uncommon to make an absolute mess of it and even when you do get it right, there’s a queue of people ready to tell you how it is, in fact, not quite right enough. Let’s start with the really wrong stuff and work from there.
This seems obvious, right? I mean the implementation of a “remember me” feature is pretty fundamental, not the sort of thing easily gotten wrong, surely? Apparently not.
Let me share two anti-patterns and we’ll discuss the problems with them before talking about how to do it right. The first example comes courtesy of Black & Decker, that would be the same B&D I recently wrote about in Security is hard, insecurity is easy. Here’s the idea when you login:
That’s all pretty standard, the interesting bit comes after you log in. Let’s take a look at the cookies:
This is a fairly impressive set of cookies but it’s the highlighted ones that are genuinely interesting. These cookies won’t get set if you don’t check the “remember me” box so their function is purely to log you back in later on. My email address is pretty clearly in there, but that’s not my password, instead it appears to be some sort of impenetrable fortress of cryptographic… hang on – that’s Base64 encoding! The thing about Base64 encoding is that it’s neatly complemented by Base64 decoding which means that you can head off to somewhere like base64decode.org and do this:
Now not everyone uses Base64 as a means of “cryptography” (yes, some people do genuinely do this) and indeed it’s a perfectly legitimate means of representing data in an ASCII format, the real story here however is that this is my password sitting there in my browser in plain text. You may wonder – why is this a problem? It’s in your own browser, right? How does an attacker get it?
I’m going to talk about two very easy ways and the first relates to the earlier link about insecurity being easy. Black & Decker had exposed ELMAH logs and in those logs was every unhandled internal server exception, somewhere north of 50,000 of them at the time I reported it to them. When ELMAH logs an exception it also logs all the request headers which means the cookies are logged. Combine that with the huge number of unhandled exceptions being thrown by the system and now you have a treasure trove of user credentials. Yes, they should have secured their ELMAH log properly to begin with but it’s a good example of how you can be very easily undone by one very simple misconfiguration.
Here’s another one:
This is Aussie Farmers Direct and it’s a fairly typical looking log in form. Let’s log in, tell it to “remember me” then take a look at the cookies:
Oh dear, same deal again but without any Base64 encoding. Actually this one is particularly bad because you can do stuff like this:
XSS your own JSON cookie? Sure! The other idiosyncrasy is that changing your password doesn’t change the cookie so you come back to the site later on and it tries to log you in with the old one. Oops.
Aussie Farmers Direct doesn’t have exposed ELMAH logs (being PHP kind of helps with that!) but they do have other risks such as XSS (incidentally, this has been responsibly disclosed and the risk summarily dismissed – multiple times). The other thing about both the sites above is that those cookies holding the passwords aren’t flagged as HttpOnly, you can see this in the second column from the right in the cookie lists. What this means is that client script can access those cookies which means that if you can get a piece of XSS onto the site – just as you can on the Aussie Farmers site – you can steal cookies containing passwords if you can get a customer to load the XSS payload (and there are numerous ways of doing that). The missing HttpOnly attribute is sloppy on behalf of both sites, but the core of the issue is storing passwords in cookies which then makes them vulnerable via other oversights.
There’s one more fundamentally important reason why both these practices are negligent; they’re protecting customers’ credentials used on other sites. Every time a customer using the “remember me” feature on one of the sites above makes a request, there’s a very good chance they’re sending the username and password to their email, their eBay or their bank across the wire, sometimes in plain text, sometimes accessible via client script, always sitting there unprotected in the browser. Password reuse is rampant and whilst those damn users should take some bloody responsibility (I’m paraphrasing here!), we – as developers – must acknowledge that we’re protecting far more than just our own site when we handle credentials.
So that should establish that there is a genuine misunderstanding of how the “remember me” feature should be built, let’s now move onto the good practices.
A sample reference implementation
One of the mantras you frequently hear in the security world is “Don’t roll your own – use what’s already been proven to be robust”. This is very frequently applied to encryption and authentication schemes but in this case we can extend this to the “remember me” feature in order to start with a good reference implementation before we delve into the details.
In a new ASP.NET MVC 4 website provisioned with Visual Studio 2012 you get this right out of the box:
Other frameworks have other standard modes of implementing this feature but this is an easy one for me to refer to. When we login with the fields as above (i.e. not asking it to remember me), successful authentication results in the following cookie being returned:
Set-Cookie:.ASPXAUTH=6891A5EAF17A9C35B51C4ED3C473FBA294187C97B758880F9A56E3D335E2F020B86A85E1D0074BDAB2E1C9DBE590AF67895C0F989BA137E292035A3093A702DEC9D0D8089E1D007089F75A77D1B2A79CAA800E8F62D3D807CBB86779DB52F012; path=/; HttpOnly
This is simply an auth cookie and in the stateless world that is HTTP it’s the one little piece of data that ties all the otherwise entirely independent requests from one person together. Each time this unique-to-me cookie is sent, the website knows it’s me and that I’ve already authenticated. We can see it bit more clearly when we look at it in Chrome’s Cookies collection:
Incidentally, the second cookie is an anti-forgery token to prevent CSRF attacks and has nothing to do with our authenticated state. Other than that, there are no other cookies.
Let’s now log in and ask the site to remember me, here’s the cookie response:
Set-Cookie:.ASPXAUTH=3A92DAC7EFF4EE5B2027A13DD8ABEA8254F0A16D8059FCAF60F5533F1B7D99462DDF57320D069A493481978750526DF952D5C9EA0371C84F5CF1BFC0CCA024C2052824D4BA09670A42B85AEC7FFCB4088FC744C6C0A22749F07AF6E65E674A4A; expires=Tue, 02-Jul-2013 00:27:05 GMT; path=/; HttpOnly
Aha – you see that?! It becomes clearer when you see it broken down in Chrome:
We now have a cookie expiration which is 48 hours from now as opposed to having no cookie expiration which means it will be discarded when the browser is closed. Let’s take a closer look at that.
Looking at auth cookie expiration
This is actually a ridiculously easy security construct and it’s only in light of the earlier examples that I’d even think it worth writing about, but here we are. In this implementation, the “remember me” feature simply boils down to when the auth cookie expires because what you’re really doing here is controlling how long you want someone to stay logged on for, it’s that simple.
In the example above, ASP.NET defaults to using a session cookie or in other words, a cookie that does not have an explicit expiration date and will therefore forcibly expire when the browser is closed. That’s one approach, another is to explicitly set a short expiration period so that even if the browser is left open the user will be automatically logged out after a period of time. Of course you can also control this behaviour on the server and you can also keep extending the lifetime of an auth cookie if the system is being actively used by the server increasing the expiration date on response.
Remembering someone can be as simple as keeping that auth cookie alive. How long should it be kept alive for? The example above defaults to two days which is probably a bit short for many legitimate uses of the feature (although it’s easily configurable in ASP.NET), try Facebook though and you’ll get cookies that last for a year. Shorter duration means less risk but more inconvenience, longer duration makes it easier for the user but increases the window of potential attack. Let’s take a look at that risk in more detail.
Exploiting long-running authentication state
While you are not authenticated, your session can’t be hijacked. I know, insightful stuff! But seriously, take a case like Aussie Farmers Direct above; that cookie will expire in six months and combined with the fact that it’s not flagged as HTTP only and that they have XSS flaws in the site there’s now half a year window where just following a link could cause me to inadvertently hand over my credentials to an attacker. If that period was, say, one month yes, they’d still have some serious flaws in their design but then the window of opportunity for an attacker has just been slashed.
On the other hand, Black & Decker has a relative short one week expiration period so in their case with the exposed ELMAH logs, yes, there was a series of bad failures on their part but unless someone had logged in with the “remember me” box ticked in the last week and caused an unhandled exception, the credentials wouldn’t be put on public display. Ok, if you’re on the site to begin with there’s a good chance you’re already logged in (at least compared to the Farmers example where you wouldn’t have to consciously go anywhere near the site to lose your password) but you can see how that risk profile changes with the cookie expiration period.
Of course underlying all this is that the auth cookie needs to be carefully protected; HttpOnly and secure attributes are an absolute must. All your classic hijacking threats remain relevant but then again, however you slice and dice this feature you’re going to end up with a cookie dependency therefore you’re going to need to watch those cookies very, very carefully.
Ultimately it’s a trade-off that needs to consider factors such as the value to an attacker of the user’s data on the site, the barrier that not being authenticated poses to a returning user and the security profile of the rest of the site. For example, Facebook has some very useful data of a social nature and users expect a low-friction (even no-friction) process when returning, plus they’ve made massive investments in their security profile. Aussie Farmer’s, on the other hand, holds personally identifiable user data plus financial info whilst providing a service that people expect to authenticate to (payment facilities) yet have little understanding of important security concepts. They’re very different risk profiles and they should be very different auth cookie expiration strategies.
Strengthening the approach
There will be folks who look at the auth cookie approach and roll their eyes in despair. The thing about security is that there’s always a better way depending on the time / money / complexity you’re willing to add and there’s also always someone ready to tell you you’ve got it wrong! Using the auth cookie expiration as a starting point, let’s look at some possible ways of strengthening the approach.
One argument against long expiration of auth cookies is that they’re effectively keeping the user authenticated and at risk of attacks such as CSRF or clickjacking. Of course the application needs to exhibit other risks in order for an attacker to capitalise on the long lasting cookie but the whole defence in depth argument comes up again. An alternative is to have a dedicated cookie keyed against the user which after validating the authenticity of it with the server, can then initiate a new authenticated session on their return. The original session can then expire quickly, the trick is to re-instate a new one when the user comes back with the dedicated “remember me” cookie and include some additional validation in the process.
One way of providing additional validation is by including the user’s IP address / user agent / other distinguishing feature in the “remember me” cookie. The rationale is that this offers some defence against hijacking of the cookie. The problem, of course, is that there are legitimate use cases where these change. In the mobile world in particular it’s not uncommon to return to a site with a different IP. Even in the world of physical connections you can’t necessarily rely on the ISP providing a static IP address. As for user agents, with browsers like Chrome and Firefox updating at what seems like every other day, unless you’re a bit selective about what attributes of the user agent string you’re storing and comparing that’s going to be a risky approach. The same defences around using unique user attributes to and add security are often discussed in the context of session and auth cookies and whilst I’m sure there are valid use cases for this (back to banking again), I can’t say I’ve actually seen it in place in any of the sites I’ve tested before. There’s probably a good reason for that…
A more pragmatic mitigation is to still separate the auth cookie from a dedicated “remember me” cookie and use the latter to re-authenticate the user but impose some restrictions. The reality is that the process of automatically persisting someone’s authenticated state – through whatever means – introduces compromises to the security model. A mitigation would be to require an automatically re-authenticated user to expressly provide their credentials again before viewing certain classes of data or performing certain activities. This is not unheard of even outside the scope of “remember me”, you might see it when performing high-value activities such as a bank transfer. In this case, we’re saying that the authenticated user is at greater risk because there’s more likelihood that they’re not who they say they are (i.e. someone else has sat down at the PC and effectively highjacked a session).
The other hardening we can apply to this approach is to ensure that the cookie used to remember the user is reset after it has been used. This also means invalidating it on the server side so there needs to be both uniqueness and persistence of the cookie value, for example a nonce persisted in both the database and the cookie. This has the benefit of ensuring that if the cookie is obtained by an attacker it has only has a single use scope – and that’s if the user can’t legitimately use it before them. There’s a good little article here which talks about some mitigations to this pattern and again, there are use cases where this can be beneficial but you are going to invest additional effort building it and there are cases where it will inconvenience legitimate users (i.e. trying to remember yourself on the same site across multiple PCs).
The last thing worth mentioning is that the same account management principles that need to be considered for active authenticated sessions are relevant in the “remember me” context. For example, are multiple simultaneously authenticated sessions allowed from the one user? If the user changes their password will it disconnect the other sessions? Can an administrator end the authenticated session? There are all sorts of issues that come up that are really part of the discussion on how authenticated sessions are verified and managed, the discussion here is merely about how to reinstate that.
When shouldn’t you allow “remember me”? (and some middle-ground alternatives)
Sometimes it simply never makes sense to allow an authenticated user to remain authenticated for long periods of time. Banking, for example, is the stereotypical use case for when you want to force re-authentication as soon as possible as the risks are just too significant to leave unused authenticated browser sessions lying around the place.
But there is actually some middle ground that can be taken here:
This is not entirely what it looks like – when you come back after an expired session where you asked the site to “Remember Me”, you’ll get this:
I’ve not obfuscated the username, the username as it appears above with the stars are stored in a cookie that lasts for three months along with some other data that inevitably identifies the user. Frankly, I don’t see a lot of value in this, remembering your username isn’t usually the problem!
But it also doesn’t have to be all or nothing, there’s middle ground. For example, the point I made earlier about re-authenticating before key processes are performed if the session had been resumed by a “remember me” feature. It’s a bit of the best of both worlds.
This is one of those features that seems like a good idea at the time (and sometimes it is) and it’s usually very easy to get right, at least sufficiently right for most purposes. Frankly, it’s still a little baffling how wrong the two earlier examples got it particularly when you consider that it would have been sufficient to just extend the lifetime of the cookie they already have!
The other point to take away from this post beyond just the mechanics of the “remember me” feature is how security is such a multi-tiered, inter-dependent beast. You might be able to get away with credentials in cookies by themselves, but combine that with the ELMAH situation or missing HTTP only attributes and XSS flaws and suddenly a foolish albeit relatively innocuous practice becomes a serious risk. What was that about “defence in depth” again?!