← All guides
Medium

Cookie security: protecting the thing that keeps customers logged in

Three small flags on a cookie decide whether a logged-in session can be stolen. What Secure, HttpOnly and SameSite each do.

7 minute read · Browser protections

The short version

When a customer logs in to your site, the thing that keeps them logged in is a cookie: a small token their browser sends back with every request. Whoever holds that token *is* that customer, as far as your site is concerned — no password needed. Three small flags on the cookie (Secure, HttpOnly and SameSite) decide how hard it is for someone else to get hold of it. Most sites set some of them. Few set all three.

Why the session cookie is the crown jewel

Passwords are checked once, at login. After that, your site hands the browser a session cookie — effectively a temporary key — and trusts anyone who presents it. That is convenient and universal, but it means an attacker does not need to steal a password; stealing the cookie is just as good and often easier. The cookie flags exist to make each of the known ways of stealing it fail.

Secure: never send it in the clear

Without the Secure flag, the browser will send the cookie over plain HTTP as well as HTTPS. So if a visitor ever loads a single http:// page of your site — by typing the address, from an old link, or because an attacker on the same Wi-Fi nudged them there — the cookie travels unencrypted and can be read off the network. With Secure, the browser simply never sends it except over HTTPS.

If your whole site is HTTPS (and it should be — see our guide to forcing HTTPS), there is no downside to this flag at all.

HttpOnly: keep it away from scripts

Normally, JavaScript running on your page can read your cookies. That means the classic injected-script attack (cross-site scripting, or XSS — the thing a Content Security Policy guards against) can grab the session cookie and send it to the attacker, who then logs in as your customer.

The HttpOnly flag makes the cookie invisible to JavaScript. The browser still sends it with every request, so your site works exactly as before, but a script on the page cannot read it. It turns a successful XSS attack from "steal every logged-in user" into something far less damaging. There is almost never a reason for a session cookie to be readable by scripts.

SameSite: do not send it on requests from other sites

Here is a subtle one. If a visitor is logged in to your site and then visits an attacker's page, that page can make their browser send a request *to your site* — submitting a form, say — and the browser attaches the session cookie automatically. Your site sees a valid logged-in request and acts on it: changes their email address, places an order, transfers money. The visitor clicked nothing on your site. This is cross-site request forgery (CSRF), and for years every web application needed special tokens to defend against it.

The SameSite flag tells the browser when to include the cookie on requests that *originate from another site*:

  • SameSite=Strict — never include it on cross-site requests. Strongest, but a visitor following a link to your site from an email will arrive logged out until they refresh.
  • SameSite=Lax — include it when the visitor navigates to your site by clicking a link, but not on hidden form posts or background requests from other sites. This is the sensible default and blocks the attack described above.
  • SameSite=None — always include it (requires Secure). Only needed for cookies that genuinely must work inside other sites' pages, such as embedded widgets or payment frames.

Modern browsers treat a cookie with no SameSite attribute as Lax by default, but that behaviour varies and is not something to rely on — set it explicitly.

Cookie scope: not too wide

A cookie also has a Domain. Set to yourbusiness.co.uk, it is sent to every subdomain — shop., blog., old-test-site. — so a weakness on any one of them exposes the cookie for all of them. Unless you specifically need the login to work across subdomains, leave Domain out so the cookie is sent only to the exact host that set it. We flag session cookies scoped to the whole domain, particularly when the domain has many subdomains.

How to set the flags

In PHP, the cleanest approach is in the session configuration, before session_start():

ini_set('session.cookie_secure', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
session_start();

For other cookies you set yourself, use the options array form of setcookie():

setcookie('remember', $token, [
    'expires'  => time() + 86400 * 30,
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);

WordPress sets HttpOnly by default and Secure when the site URL is HTTPS; a security plugin can add SameSite. On cPanel/Apache hosting, a blanket fallback for anything the application misses is possible in .htaccess: Header always edit Set-Cookie ^(.*)$ "$1; Secure; HttpOnly; SameSite=Lax" — but it is a blunt tool, and will break any cookie that genuinely needs to be read by JavaScript, so prefer fixing the application.

If a session cookie has been exposed — say the site ran over plain HTTP for a while without the Secure flag — the right response is to invalidate all existing sessions (log everyone out) after fixing the flags, so any tokens that were captured stop working.

What VigilScan checks

We look at every cookie your homepage sets, identify which look like session or authentication cookies (by name and shape), and check each for Secure, HttpOnly and SameSite. We also check whether the Domain is set wider than it needs to be. The report shows the cookie name and the exact attributes we saw, so you can match it to your code.

Common questions

My cookie banner plugin sets cookies without these flags. Does that matter?
Much less. The flags matter most on cookies that grant access — sessions, logins, "remember me" tokens. A consent-preference cookie holds nothing worth stealing. We prioritise findings on cookies that look like sessions.
Will SameSite=Lax break "login with Google" or payment redirects?
Usually not — those flows end with a top-level navigation back to your site, which Lax allows. If a flow relies on a cross-site POST landing on your site with the session attached, you may need SameSite=None on that specific cookie, with Secure. Test the flow after changing it.
If I set all three flags, do I still need CSRF tokens?
SameSite=Lax blocks the common forms of CSRF, but defence in depth is wise: keep the CSRF tokens your framework provides. Older browsers and some edge cases (such as GET requests that change state, which should not exist anyway) are not covered by SameSite alone.

Not sure if this affects your site?

Run a free check — it takes about 90 seconds and tells you exactly where you stand.

Check my website