Ahmed Hinedy

HTTP Cookies Demystified: A Web Developer's Guide

A practical guide to HTTP cookies: how Set-Cookie and Cookie headers work, the security attributes that matter, and when to reach for localStorage instead.

HTTP cookies demystified: a web developer's guide

HTTP cookies are headers. They ride in the Cookie header on requests and the Set-Cookie header on responses, and their security flags decide whether they're safe for authentication data.

Cookies are small pieces of data the browser stores on a user's device. They keep sessions alive and carry state between requests.

Cookies vs localStorage

When it comes to client-side storage, developers often choose between cookies and localStorage. Let's compare these options:

localStorage is accessible to JavaScript, which makes it convenient for storing client-side data. However, this accessibility comes with a significant drawback: it doesn't provide any protection against Cross-Site Scripting (XSS) attacks. There are multiple ways an attacker could exploit an XSS vulnerability to access data stored in localStorage.

On the other hand, cookies have security flags that make them more secure for storing sensitive data:

  • HttpOnly flag prevents client-side JavaScript from accessing the cookie, mitigating XSS risks.
  • Secure flag ensures that the browser only transfers the cookie over SSL, protecting against man-in-the-middle attacks.
  • SameSite flag helps prevent Cross-Site Request Forgery (CSRF) attacks by ensuring that the cookie is only sent to the origin site.

So cookies are the safer choice for authentication data. localStorage fits non-sensitive data like user preferences or cached content. It holds more (usually 5-10MB versus 4KB for cookies) and JavaScript can read it directly, which helps when the security of cookies isn't needed.

How cookies work

Cookie exchange between client and server

  1. Setting the Cookie: When a user visits a website, the backend server can send a cookie to the browser. This is done via the Set-Cookie header in the HTTP response.
  2. Storing the Cookie: The browser automatically stores these cookies. As a developer, you don't need to write any client-side code to save the cookie.
  3. Accessing Cookies: On the client side, you can access some of the cookies (not HttpOnly) using document.cookie. For example:
    console.log(document.cookie);
    // Output: "username=John Doe; session_id=1234567890"
  4. Sending Cookies: The browser automatically sends all relevant cookies (based on domain and path) with every request to the server. This includes regular page loads, AJAX calls, and resource requests like images or scripts from the same domain.

There's an exception: cookies aren't automatically sent with cross-origin POST requests. To include them, enable the withCredentials option in your client-side API request headers, as shown

axios
  .post(
    "https://api.example.com/data",
    {
      key: "value",
    },
    {
      withCredentials: true,
    },
  )
  .then((response) => console.log(response))
  .catch((error) => console.error("Error:", error));

Keep in mind that this requires the server to respond with the appropriate CORS headers, including Access-Control-Allow-Credentials: true. If the server does not allow credentials, the browser will still prevent the inclusion of cookies, even if you set withCredentials: true on the client side.

Cookie attributes

Cookies can have various attributes that control their behavior:

  • Expires: Determines how long the cookie should last.
  • Domain: Specifies which domains the cookie is valid for.
  • Path: Limits the cookie to a specific path on the server.
  • Secure: Ensures the cookie is only sent over HTTPS.
  • HttpOnly: Prevents JavaScript from accessing the cookie.
  • SameSite: Controls how the cookie is sent with cross-site requests.

The IETF defines the cookie spec: Learn more.

Setting cookies

The method of setting a cookie depends on the context:

HTTP headers

This is the common server-side approach. Servers send a Set-Cookie header in the response, containing cookie details such as name, value, and various attributes like expiry time and domain. This is typically done in server-side languages like PHP, Python, or JavaScript(Express).

app.get("/set-cookie", (req, res) => {
  res.setHeader("Set-Cookie", [
    "user=John; HttpOnly; Secure; SameSite=Strict",
    "session=1234567890; HttpOnly; Secure; SameSite=Strict; Max-Age=3600",
  ]);
  res.send("Cookies are set");
});

JavaScript

JavaScript, through the document.cookie property, allows you to set cookies directly on the client-side.

document.cookie = "username=John Doe; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/";

However, this approach has limits: it can't set attributes like HttpOnly, and those are the ones that matter for security.

Refer to MDN's cookie documentation for more details and examples.

Cookie best practices

When working with cookies, consider these best practices:

  1. Use the HttpOnly flag for session cookies to prevent XSS attacks.
  2. Use the Secure flag to ensure cookies are only transmitted over HTTPS.
  3. Implement proper CSRF protection for cookies that are used for session management.
  4. Use the SameSite attribute to prevent CSRF and information leakage.
  5. Don't store sensitive information in cookies unless it's absolutely necessary.
  6. Be mindful of cookie size limitations (generally 4KB per cookie).
  7. Consider the implications of EU cookie laws and obtain user consent when necessary.