HTTP protocol itself is stateless. That means when a server receives two requests it cannot tell whether they originate from the same user. There is no conversation context preserved between requests. While it has many advantages such as good scalability, often you want to identify a user you already interacted with and keep state. For example, a user should usually be able to log-in and after that, all their requests should be considered as those of an authenticated user. In other words, you want to recognize such user when they send their next request.
But how can you achieve this? One way is using Cookies. Whenever a server receives a request from a client, it can provide a special HTTP response header called
Set-Cookie. It can look something like this:
What it basically says is: Here is a little piece of information (cookies are key-value pairs). I want you to remember it. And I want you to send it back to me with your future requests. So I know it is you. The client's browser receives this information and stores the cookie on the client's machine. Whenever that client makes a request to the server, the browser checks all the cookies, selects these which are relevant to the current server and sends them as HTTP headers.
Okay, so cookies can be used to store information on the client and then sent to a server with each request. Information such as the unique identifier of a logged in user. And it is sent in the form of HTTP header. Now imagine someone gets their dirty hands on your precious cookie. Since your session cookie represents your identity, the attacker can use it to impersonate you. To perform restricted actions as if they were you. Transfer money, steal sensitive information, delete important data, this kind of stuff. This means you need to make sure your cookies are as protected as possible. So what are the ways you can mitigate the risk of someone stealing your cookies?
let cookies = document.cookie;
Not only it is easy to steal cookies this way, but the attacker can tamper with existing cookies or create new ones:
document.cookie = "my_precious_cookie=some_harmful_value";
Set-Cookie: [cookie_name]=[cookie_value]; HttpOnly
You should use HTTPS of course, so your data is secure even in-transit. This way man-in-the-middle cannot read your cookies. Unless... Well, there are ways a client can access your HTTPS site using regular HTTP. Maybe they manually type http://. Maybe you have mixed content. Maybe you do use HTTP Strict Transport Security to enforce HTTPS, but the client's browser does not support it yet. Either way, you risk your precious cookies transmitted over an unsecured channel.
Fortunately, there is a way to prevent this. Similar to
HttpOnly, there is another attribute that instructs the browser that it should only send the cookie over a secured channel.
Set-Cookie: [cookie_name]=[cookie_value]; Secure
When a browser reads a page, there are usually some resources from other domains, which are loaded as well such as images, scripts or social media buttons. The default browser behavior is that it sends cookies which browser has for the external site with the request. Since cookies are sent with cross-domain requests, this can be exploited to trick a previously authenticated user into performing some restricted action on the external site. This type of attack is called Cross-Site Request Forgery (or CSRF).
Fortunately, when setting a cookie, you can specify that browser should send it to you only if the request is originating from the same origin (is not cross-domain). This cookie parameter is called
SameSite. It has two possible values:
Strict option prevents sending cookies from different origin altogether. While this is safe, it can be really painful for the users. For example, if you click a link to a social media or GitHub, you would be redirected there without being logged in and you would need to authenticate again.
The second option,
lax, is more forgiving. It will allow sending the cookie cross-origin as long as the HTTP method is GET only, and you are navigating to the root (top level).
Fortunately, as of 11/2023 the browser support is now very good.
Recently, based on the IETF proposal Incrementally Better Cookies, the behavior of SameSite Cookies has changed.
Originally, if you didn't specify the
SameSite attribute, it would allow cross-site cookie sharing without limitations. The new behavior is as follows:
- The default behavior is
Laxif not specified otherwise.
- There is new option
- Cookies with
This should result in increased security. There are additional changes in how browsers should treat cookies:
- Going from
httpson the same site is considered cross-site
- Cookies on
httpson the same site are considered distinct
This new behavior is still not completely supported by all the browsers as of 11/2023. Chrome supports this, starting with version 80, the same goes with Edge. In Firefox, you can explicitly enable the support since version 69, however, it is not enabled by default. You can still use
Samesite=None as it is backward compatible and will be ignored by older browsers.
By default, when not specified otherwise, a cookie is only sent to a sub-domain, which set the cookie. That means that if you set a cookie from account.example.com it will not be sent to forum.example.com. You can, however, specify the Domain attribute of the cookie.
Set-Cookie: [cookie_name]=[cookie_value]; Domain=account.example.com
If you specify just a root domain (example.com) and not the subdomain (account.example.com), the cookie will be sent to ANY subdomain of the root domain. Don't do this as you want to minimize the number of places your cookies are sent to.
There is another parameter which can be used to define the scope of the cookie, similar to Domain. It is called Path. It is used to define resources under the domain, to which the cookie will be sent. For example, path
/ means everything,
/blog means every resource starting with 'blog', eg:
By default, if unspecified, it is set to the path of the resource which set the cookie. Be careful not to set manually a value which is too permissive. When you set
/ that means that all the resources on the given domain will receive the cookie. This can be dangerous when you have multiple applications sitting on the same domain which are different only in their path:
In this case, the apps would be sharing cookies. That means that if one of them is vulnerable it can make the other one vulnerable as well as you could steal the other app's cookies. For example using Cross Site Scripting.
In the same way you want to reduce the cookie's scope to minimize the number of places where it can be stolen, you want to minimize the cookie's lifespan to decrease the time when the cookie is vulnerable.
Cookie lifespan is, if not specified, just for one session. That means until the user closes their browser. Well, at least in theory. These days browsers usually have some mechanisms for session restoration or continue to run in the background, so you cannot depend on session cookies being immediately wiped.
You can specify cookie expiration or lifespan using
For sensitive cookies use the shortest sufficient lifespan possible. Using these parameters you can set cookie lifespan to infinity, which is obviously not a good idea. It is also worth mentioning that to further limit the lifespan of your session cookies, you should limit session duration server side and provide your users with a means of manually terminating their session (logout).
Cookies are also vulnerable during transport, so you should apply HTTPS and make sure your sensitive cookies are not transmitted using plain HTTP by specifying Secured cookie attribute.
To defend against Cross Site Request Forgery, you should add the SameSite attribute with either strict or lax value.
The limit attack vectors you should keep the availability of your cookie at the minimum. That means limiting the scope (Domain and Path) and lifespan (Expires and Max-Age).