Security Considerations for Cookie-Based Authentication

When a user logs in to a web application, usually a server-side program records a new user session in a database and returns a unique ID for the session, which the client-side application stores in a cookie. The session cookie thereafter acts as a proxy for the user's username and password, so she does not need to log in again with every new HTTP request.

Session cookies are convenient but they introduce security concerns for application developers. If a hacker can get his hands on a valid session ID, he can impersonate another user and gain access to all of the user's protected resources. This is called session hijacking.

There are three main exploit mechanisms by which session cookies can be stolen:

  • Man-in-the-middle (MITM)
  • Cross-site request forgery (XSRF)
  • Cross-site scripting (XSS)

Man-in-the-middle (MITM)

A man-in-the-middle attack is when someone listens in to the traffic between a web browser and server. The middleman grabs data from the HTTP requests and returns seemingly legitimate responses. The user is none-the-wiser that his interactions with the web service are being intercepted.

Encrypting all communications by using HTTPS resolves most MITM attacks. If passwords and other sensitive information are posted over standard HTTP, they are effectively delivered in the clear and anyone with the right bit of software can listen in and hijack a users' session and any other personal data that is sent over the wire.

It is a good idea to set the Secure flag on session cookies, too. This stops browsers from transmitting the cookies over unencrypted connections and thus acts as a fail-safe in the event that a secure connection gets unexpectedly downgraded to standard HTTP.

Be careful also where you save log files, database dumps and backups. These often contain private data including session IDs, so must not be stored in publicly-accessible places.

Cross-site request forgery (XSRF)

XSRF attacks occur when a malicious web page links to a resource on another trusted website that a user is already logged in to. The trusted resource is typically loaded from an <img> tag embedded in the exploit page:

<img src="http://trustedsite.com/vote?score=10&itemid=12345" height="0" width="0" />

When the trusted resource is requested, the browser unwittingly sends along any cookies for the trusted domain, including session cookies. As a result the trusted site will verify the request against the session cookie and perform an unwanted action.

This is why the convention has been established that GET and HEAD requests should never trigger any creative or destructive processes. However, APIs accessible via the POST method are susceptible to XSRF attacks, too. In this case the request is made via an HTML form embedded in the exploit page.

And if a target website accepts requests from any origin and via any HTTP method, then no APIs will be safe from XSRF attacks. Here's how an exploit page may trigger a cross-origin PUT request:

<script>
    function put() {
        var x = new XMLHttpRequest();
        x.open('PUT', 'http://trustedsite.com/vote', true);
        x.setRequestHeader('Content-Type', 'application/json');
        x.send(JSON.stringify({'score': 10, 'itemid': 12345}));
    }
</script>
<body onload="put()">

XSRF attacks are commonly used to hijack a verified user's account in order to post spammy messages on forums, microblogging services and comment feeds. In some cases XSRF attacks can be used to steal personal user data, too. Specifically, XSRF exploits can sometimes learn to read JSON data returned in the body of HTTP responses. Because JSON is a strict subset of JavaScript, JSON data can be stripped from HTTP responses and loaded into script tags and executed. The data can thus be parsed and forwarded on to the attacker.

XSRF attacks can be partially prevented by checking a request's Origin header and allowing only requests that originate from the same base URL as the web application. No modern browser (IE9+) allows Origin headers to be modified by JavaScript, giving you a high degree of confidence that Origin information is valid. But it's not a perfect safeguard. A combination of browser plugins and redirects can still allow attackers to forge Origin headers. And this solution doesn't help web services that are designed to accept cross-origin requests.

Effective mechanisms to counter XSRF attacks work by requiring anti-forgery tokens to be embedded into requests, allowing server-side applications to detect requests from unauthorised locations.

There are two main anti-forgery mechanisms.

The first is a synchroniser token. The server-side application generates a secret and unique value for each request. This value is embedded by the server-side application in a hidden field in all HTML forms. The value is verified on the server when a form is submitted.

<input type="hidden" name="token" value="4zCf0AN0AcNpfY2ZWpILevXAZLvbwgtYpErfif5y" />

The second mechanism is something called a double-submit token. This is more useful for securing single-page applications and web APIs – anything where client-side JavaScript is making the HTTP requests. Again, the server generates a unique value, which this time is delivered to the browser as a cookie.

Set-Cookie: xsrf-token=4zCf0AN0AcNpfY2ZWpILevXAZLvbwgtYpErfif5y; expires=Thu, 29-Oct-2015 21:25:13 GMT; Max-Age=31449600; Path=/

The next request is expected to return the value, which is copied by JavaScript from the cookie into an HTTP header.

X-XSRF-Token: 4zCf0AN0AcNpfY2ZWpILevXAZLvbwgtYpErfif5y

The security of this technique is based on the fact that only a JavaScript program served from the same origin can read the cookie's value. JavaScript running from a rogue web page hosted elsewhere will not be able to read the cookie. The token cookie will be sent to the server automatically with every request, but that's OK because the server will be looking for a valid token header and will pay no attention to the token cookie.

XSRF token cookies must not use the HttpOnly flag, since this will lock out JavaScript from accessing the cookie.

In both of these anti-forgery mechanisms, short-lived one-time-use tokens are the best. But this can introduce usability problems, for example when a user opens a website in multiple tabs. And it doesn't work so well in web applications that make heavy use of asynchronous requests. Many developers compromise a little on security by issuing anti-forgery tokens that last for whole sessions rather than until the next request.

JSON responses can be protected by prepending JSON strings with code that makes it non-executable. For example, you could put in a statement that throws an exception. Putting the whole JSON string in a comment block works, too. JSON obfuscation is easiest to apply as a blanket policy across all server-side and client-side components that transmit and parse JSON. In effect, you develop a unique and proprietary wrapper for all JSON strings served by your application.

Letting sessions expire after short periods of inactivity offers further protection against XSRF exploits, since it narrows a hacker's window of opportunity. And demanding that users resupply their login credentials to confirm any state-changing operations is the ultimate defence against XSRF.

Cross-site scripting (XSS)

XSS is the most commonly reported security breach. XSS is a type of vulnerability in which an attacker injects some JavaScript into a web page, which is then executed by the browser.

Historically, malicious JavaScript has been injected via forms. Nowadays the greater risk comes from scripts that are served via content delivery networks (CDNs). Think about all of the JavaScript that you add to your web pages to load web fonts, to generate usage data, to enable social sharing, and to serve generic JavaScript frameworks such as jQuery. If any one of those third-party services is compromised, your application's entire JavaScript environment will be wide open to the attacker.

As with XSRF, a multi-layered security plan is required to protect an application from XSS exploits. At an absolute minimum, all user input should be sanitised to remove any executable code before it is stored anywhere. And all user-generated content should be escaped before being embedded in HTML documents.

Using HttpOnly cookies prevents the JavaScript environment from accessing cookie data. In the event of a successful XSS attack, at least the malicious program will not be able to access anyone's session ID and so hijack their session.

For the same reason, session data and sensitive information should not be saved in web storage, which is always accessible via JavaScript APIs.