If you're a JavaScript developer and you want to learn about browser cookies and what you can do with them, you're at the right place. This article will cover how browser cookies work, how you can access and manipulate them both from the client and server, and how to control their visibility across browsers using their attributes.

What are cookies and how do they work?

A browser cookie is a small piece of data stored on a browser that's created either by the client-side JavaScript or a server during an HTTP request. The browser can then send that cookie back with requests to the same server and/or let the client-side JavaScript of the webpage access the cookie when a user revisits the page.

Cookies are generally used for session management, personalization (such as themes or similar settings), and tracking user behavior across websites.

There was a time when cookies were used for all kinds of client-side storage, but there was an issue with this approach.

Since all domain cookies are sent with every request to the server on that domain, they can significantly affect performance, especially with low-bandwidth mobile data connections. For the same reason, browsers also typically set limits for cookie size and the number of cookies allowed for a particular domain (typically 4kb and 20 cookies per domain).

With the modern web, we got the new Web Storage APIs (localStorage and sessionStorage) for client-side storage, which allows browsers to store client-side data in the form of key-value pairs.

So, if you want to persist data only on the client-side, it's better to use the APIs because they are more intuitive and easier to use than cookies and can store more data (usually up to 5MB).

Setting and accessing cookies

You can set and access cookies both via the server and the client. Cookies also have various attributes that decide where and how they can be accessed and modified. But, first, let's look at how you can access and manipulate cookies on the client and server.

Client (browser)

The JavaScript that downloads and executes on a browser whenever you visit a website is generally called the client-side JavaScript. It can access cookies via the Document property cookie.

This means you can read all the cookies that are accessible at the current location with document.cookie. It gives you a string containing a semicolon-separated list of cookies in key=value format:

const allCookies = document.cookie; // The value of allCookies would be something like // "cookie1=value1; cookie2=value2" 

Similarly, to set a cookie, we must set the value of document.cookie. Setting the cookie is also done with a string in key=value format with the attributes separated by a semicolon:

document.cookie = "hello=world; domain=example.com; Secure"; // Sets a cookie with key as hello and value as world, with // two attributes SameSite and Secure (We will be discussing these // attributes in the next section) 

Just so you're not confused, the above statement does not override any existing cookies; it just creates a new one or updates the value of an existing one if a cookie with the same name already exists.

Now, I know this is not the cleanest API you have ever seen. That's why I recommend using a wrapper or a library like js-cookie to handle client cookies:

Cookies.set('hello', 'world', { domain: 'example.com', secure: true }); Cookies.get('hello'); // -> world 

Not only does it provide a clean API for CRUD operations on cookies, it also supports TypeScript, helping you avoid any spelling mistakes with the attributes.

Server

The server can access and modify cookies via an HTTP request's response and request headers. Whenever the browser sends an HTTP request to the server, it attaches all the relevant cookies to that site with the cookie header.

Check the request headers of almost any web app you use, and you'll find the cookies sent to the server with request headers as a semicolon-separated string.

Cookies Sent Server Request Headers

Cookies being sent to the server with request headers

You can then read these cookies on the server from the request headers. For example, if you use Node.js on the server, you can read the cookies from the request object, like the snippet below, and get the semicolon-separated key=value pairs, similar to what we saw in the previous section:

http.createServer(function (request, response) {   var cookies = request.headers.cookie;   // "cookie1=value1; cookie2=value2"   ... }).listen(8124); 

Similarly, to set a cookie, you can add a Set-Cookie header with the response headers in key=value format with attributes separated by a semicolon, if any. This is how you can do it in Node.js:

response.writeHead(200, {    'Set-Cookie': 'mycookie=test; domain=example.com; Secure' }); 

Also, chances are you won't use plain Node.js; instead, you can use it with a web framework like Express.js.

Accessing and modifying cookies gets much easier with Express by adding middleware. For reading, add cookie-parser to get all the cookies in the form of a JavaScript object with req.cookies. You can also use the built-in res.cookie() method that comes with Express for setting cookies:

var express = require('express') var cookieParser = require('cookie-parser')  var app = express() app.use(cookieParser())  app.get('/', function (req, res) {   console.log('Cookies: ', req.cookies)   // Cookies: { cookie1: 'value1', cookie2: 'value2' }    res.cookie('name', 'tobi', { domain: 'example.com', secure: true }) })  app.listen(8080) 

And yes, all of this is supported with TypeScript, so there is no chance of typos on the server as well.

JavaScript cookie attributes

Now that you know how you can set and access cookies, let's dive into the attributes of cookies.

Apart from name and value, cookies have attributes that control a variety of aspects that include cookie security, cookie lifetime, and where and how they can be accessed in a browser.

Domain attribute

According to MDN, the Domainattribute tells a browser which hosts are allowed to access a cookie. If unspecified, it defaults to the same host that set the cookie.

So, when accessing a cookie using client-side JavaScript, only the cookies that have the same domain as the one in the URL bar are accessible.

Similarly, only the cookies that share the same domain as the HTTP request's domain are sent along with the request headers to the server.

Remember that having this attribute doesn't mean you can set cookies for any domain because that would obviously be a huge security risk. (Imagine an attacker on evil.com modifying the cookies for your site, awesome.com, when the user visits their website.)

So, the only reason this attribute exists is to make the domain less restrictive and make the cookie accessible on subdomains.

For example, if your current domain is abc.xyz.com, and you don't specify the domain attribute when setting a cookie, it would default to abc.xyz.com, and the cookies would only be restricted to that domain.

But, you might want the same cookie to be available on other subdomains as well. If this is the case, set Domain=xyz.com to make it available on other subdomains like def.xyz.com and the primary domain xyz.com.

Domain attribute allowing cookies to be accessed via subdomains

However, this does not mean that you can set any domain value for cookies; top-level domains (TLDs) like .com and pseudo TLDs like .co.uk would be ignored by a well-secured browser.

Initially, browser vendors maintained lists of these public domains internally, which inevitably caused inconsistent behavior across browsers.

To tackle this, the Mozilla Foundation started a project called the Public Suffix List that records all public domains and shares them across vendors.

This list also includes services like github.io and vercel.app that restricts anyone from setting cookies for these domains, making abc.vercel.app and def.vercel.app count as separate sites with their own separate set of cookies.

Path attribute

The Path attribute specifies the path in the request URL that must be present to access the cookie. Apart from restricting cookies to domains, you can also restrict them via path. A cookie with the path attribute as Path=/store is only accessible on the /store path and its subpaths, /store/cart, /store/gadgets, and others.

Expires attribute

The Expires attribute sets an expiration date when cookies are destroyed. This can come in handy when you are using a cookie to check if the user saw an interstitial ad; you can set the cookie to expire in a month so the ad can show again after a month.

And guess what? It also removes cookies by setting the [Expires] date in the past.

Secure attribute

A cookie with the Secure attribute only sends to the server over the secure HTTPS protocol and never over the HTTP protocol (except on localhost). This helps prevent Man in the Middle attacks by making the cookie inaccessible over unsecured connections.

Unless you are serving your websites via an unsecured HTTP connection (which you shouldn't) you should always use this attribute with all your cookies.

HttpOnly attribute

This attribute, as the name probably suggests, allows cookies to be only accessible via the server. So, only the server can set them via the response headers. If they are sent to the server with every subsequent request's headers, they won't be accessible via client-side JavaScript.

HttpOnly Attribute Example

This can partially help secure cookies with sensitive information, like authentication tokens, from XSS attacks since any client-side script can't read the cookies. But, remember it does not guarantee complete security from XSS attacks.

This is because if the attacker can execute third-party scripts on your website, they might not be able to access the cookies, and instead, can directly execute any relevant API requests to your server , causing the browser to readily attach your secure HttpOnly cookies with the request headers.

Imagine one of your users visits a page where a hacker injected their malicious script into your website. They can execute any API with that script and act on the user's behalf without them ever knowing.

So, when people say that HttpOnly cookies cause XSS attacks to be useless, they are not completely correct because if a hacker can execute scripts on your website, you have much bigger problems to deal with. There are ways to prevent XSS attacks, but they are out of the scope of this article.

SameSite attribute

At the beginning of this article, we saw how cookies for a particular domain are sent with every request to the server for the corresponding domain.

This means that if your user visits a third-party site, and that site makes a request to APIs on your domain, then all the cookies for your domain will be sent with that request to your server. This can be both a boon and a curse depending on your use case.

This can be a boon in the case of something like YouTube embedding.

For example, if a user who is logged in to YouTube on their browser visits a third-party website containing YouTube embeds, they can click on the Watch Later button on the embedded video and add it to their library without leaving the current website.

This works because the browser sends the relevant cookies for YouTube to the server confirming their authentication status. These types of cookies are also called third-party cookies.

The curse this can cause is in basically any other use case you didn't intend it to happen.

For instance, if a user visits a malicious website where that website makes a request to your server, and if the server doesn't validate the request properly, then the attacker can take actions on the user's behalf without their knowledge. This is basically a CSRF attack.

To help prevent this type of attack, the IETF proposed in 2016 a new attribute in cookies called SameSite. This attribute helps the above problem by allowing you to restrict your cookies to only a first-party context.

This means you should only attach cookies to the request when the domain in your URL bar matches the cookie's domain.

Same Site Attribute Strict

SameSite attribute with Strict

There are three types of values you can set for the SameSite attribute: Strict, Lax, and None.

When set to Strict, your cookies will only be sent in a first-party context.

The Lax value is slightly less restrictive than Strict because it sends cookies with top-level navigations, meaning the cookie is sent to the server with the request for the page.

This is helpful in cases when a user clicks on your website from a Google search result or is redirected via a shortened URL.

Then None, as the name suggests, allows you to create third-party cookies by sending the relevant cookies with every request. This, however, is irrespective of the site user for cases like the YouTube embeds we discussed previously.

You can learn more about SameSite cookies and how they behave with modern browsers in this post on web.dev.

Privacy and third-party cookies

We briefly explained third-party cookies in the previous section. In short, any cookie set by a site other than the one you are currently on is a third-party cookie.

You may have also heard about how infamous third-party cookies are for tracking you across websites and showing personalized ads. Now that you know the rules of cookies, you can probably guess how they might do it.

Basically, whenever a website uses a script or adds an embedded object via IFrame for third-party services, that third-party service can set a cookie for that service's domain with HTTP response headers.

These cookies can also track you across websites that use the same third-party service embeds. And finally, the data collected by these third-party services by identifying you via cookies can then show you personalized ads.

To tackle this, many browsers like Firefox started blocking popular third-party tracking cookies via a new feature they call enhanced tracking protection (ETP). Although this protects users from the 3000 most common identified trackers, its protection relies on the complete and up-to-date list.

Browsers are currently planning to eventually get rid of third-party cookies. Firefox is implementing state partitioning, which will result in every third-party cookie having a separate container for every website.

Now, you might think that something like state partitioning will also break legitimate use cases for third-party cookies apart from tracking, and you're right.

So, browsers are working on a new API called Storage Access This API allows third-party context to request first-party storage access via asking users permission, which gives the service unpartitioned access to its first-party state. You can read about how it works in more detail on Mozilla's blog.

Conclusion

I hope this article helped you learn something new about JavaScript cookies and gave you a brief overview of how they work, how they can be accessed and modified from the server and the client, and lastly, how the different attributes of cookies let you control their visibility and lifespan in the browser.