A session cookie needs three flags: Secure, so it is never sent over plain HTTP; HttpOnly, so injected script cannot read it; and SameSite=Lax or Strict, so a forged cross-site request does not carry it. Prefix the name with __Host- to make the browser enforce the strictest scoping. Without these, one XSS bug or one coffee-shop network is enough to take over an account.

Why the flags exist

A cookie is a bearer token: whoever presents it is treated as the user it belongs to. That makes the session cookie the single most valuable string on your site, and the three flags each close one route by which it leaks. Without Secure, the browser will attach the cookie to an http:// request to the same host, which anyone on the network path can read; a single hard-coded HTTP link or an image loaded over HTTP is enough to trigger it. Without HttpOnly, any script running on the page, including one injected through cross-site scripting, can read document.cookie and ship the session to an attacker. Without SameSite, a form on an attacker's page that posts to your site arrives carrying the victim's cookie, which is the entire mechanism of cross-site request forgery.

The flags are cheap, they are supported everywhere, and the failure modes they prevent are among the most common ways real accounts are hijacked. There is no serious argument for leaving them off a session cookie in 2026.

What each flag does, precisely

AttributeEffectSet it on
SecureCookie is only sent on HTTPS requests (and from an HTTPS page). Cannot be set from an insecure origin at all in modern browsers.Every cookie, without exception
HttpOnlyCookie is invisible to document.cookie and to script in general. Still sent on requests as normal.Session and CSRF-secret cookies; anything script does not need to read
SameSite=StrictNever sent on any cross-site request, including a top-level navigation from a link on another site.High-value actions where arriving logged-out from an external link is acceptable
SameSite=LaxSent on top-level GET navigations from other sites; withheld on cross-site POSTs, iframes, images and fetch.Session cookies; the sensible default
SameSite=NoneSent on all cross-site requests. Rejected by browsers unless Secure is also present.Only cookies that must work inside third-party embeds
Path / DomainScope of URLs and hosts the cookie is sent to. Omitting Domain restricts the cookie to the exact host that set it.Omit Domain unless subdomains genuinely share the session
Max-Age / ExpiresLifetime. Without either, the cookie lasts for the browser session.Keep session lifetimes short; use a separate long-lived "remember me" token

Two subtleties trip people up. First, Chrome treats a cookie with no SameSite attribute as Lax, with a temporary exception that still sends it on cross-site POSTs for two minutes after it was set; Firefox and Safari do not apply the same default, so you cannot rely on browser behaviour and must set the attribute explicitly. Second, "site" in SameSite means the registrable domain plus scheme, so app.example.com and api.example.com are same-site to each other, but http://example.com and https://example.com are not.

The __Host- and __Secure- prefixes

Cookie prefixes, defined in the revised cookie specification (RFC 6265bis), let the server tell the browser to enforce attributes at the point the cookie is set, so that a misconfigured or attacker-controlled part of your domain cannot override them. A cookie whose name begins with __Secure- is rejected unless it has the Secure flag and was set from a secure origin. A cookie beginning with __Host- must additionally have Path=/ and no Domain attribute, which locks it to exactly one host.

The attack this stops is cookie tossing. Cookies from evil.sub.example.com, or from a plain HTTP page on the same host, can normally set a cookie named session for example.com that shadows the real one, which enables session fixation and some CSRF-token bypasses. A browser will simply refuse to accept a __Host-session cookie from anywhere except a secure page on the exact host, with the exact attributes. If you have subdomains you do not fully control, or user-generated content on a subdomain, the prefix is not optional.

A copy-ready Set-Cookie header

Set-Cookie: __Host-session=6f1c9e0b2a7d4e58b3c0f9a1d2e3f4a5; Secure; HttpOnly; SameSite=Lax; Path=/; Max-Age=3600

That is a complete, valid session cookie: bound to one host, HTTPS only, invisible to script, withheld from cross-site POSTs, and expiring after an hour of inactivity if the server refreshes it on each request. For a cookie that must be readable by JavaScript, such as a CSRF token used by a single-page app, drop HttpOnly and nothing else. For a cookie that must be sent inside a third-party iframe, such as an embedded checkout, you would use SameSite=None; Secure, and you should keep that cookie separate from the main session.

Setting the flags in common frameworks

Most frameworks default to safe values for some flags and unsafe values for others, and development settings that disable Secure for localhost have a way of reaching production. Set them explicitly.

# Express (express-session)
app.use(session({
  name: "__Host-session",
  cookie: { secure: true, httpOnly: true, sameSite: "lax", path: "/", maxAge: 3600000 },
  // ...
}));

# Django settings.py
SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_SECURE = True

# PHP php.ini
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
session.name = __Host-PHPSESSID

# Rails config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
  key: "__Host-_app_session", secure: true, httponly: true, same_site: :lax

# Flask
app.config.update(SESSION_COOKIE_SECURE=True, SESSION_COOKIE_HTTPONLY=True,
                  SESSION_COOKIE_SAMESITE="Lax", SESSION_COOKIE_NAME="__Host-session")
Behind a reverse proxy or load balancer that terminates TLS, the application may believe the request arrived over HTTP and refuse to set Secure cookies, or set them without the flag. Configure the framework to trust the X-Forwarded-Proto header from your proxy (Django's SECURE_PROXY_SSL_HEADER, Express's trust proxy) and verify the resulting header with curl rather than assuming.

Verifying the result with curl

Do not trust the configuration file; trust the header. Fetch the page that sets the cookie and read the raw response. If the flags are missing here, they are missing for every user, whatever the framework documentation promised.

curl -sI https://example.com/login | grep -i '^set-cookie'
# want: set-cookie: __Host-session=...; Secure; HttpOnly; SameSite=Lax; Path=/

Then log in through a browser and open the Application (Chrome) or Storage (Firefox) panel. The cookie table shows a column for each attribute, and a cookie set after authentication is the one that matters. While you are there, check the lifetime: a session cookie with a 30-day Max-Age is 30 days of exposure if it is ever stolen. Prefer short lifetimes refreshed on activity, regenerate the session identifier on login and privilege change, and invalidate it server-side on logout rather than only deleting the cookie. The flags limit how a cookie can leak; the lifetime limits how much a leak costs.

Choosing between Lax and Strict

Strict sounds better and is usually worse. With SameSite=Strict, a user who clicks a link to your dashboard from an email, a chat message or a search result arrives without their session cookie and sees the login page, even though they are logged in. Many sites respond by immediately redirecting to the same URL, which sends the cookie on the second, same-site request and defeats the purpose. Lax allows exactly the top-level GET navigations that users expect to work while withholding the cookie on every request type an attacker can use for CSRF. Our position is Lax for the session cookie, with Strict reserved for a second cookie that gates destructive actions such as changing the password or payout details, where a re-login is an acceptable price.

SameSite is a defence in depth, not a replacement for CSRF tokens. GET requests that change state, subdomain takeovers, and the Chrome two-minute Lax exception are all gaps that a synchroniser token or a signed double-submit cookie still closes. Keep both.

How a scanner sees your cookies

An external scan reads the Set-Cookie headers on the responses it receives without logging in, and reports each cookie missing a flag under Cookie without Secure flag, Cookie without HttpOnly flag, Cookie without SameSite attribute, and notes whether prefixes are in use under Cookie name prefixes. The Scan.now website scanner does this passively from a normal page fetch; it never attempts to log in, replay a session or inject anything.

That limitation matters when you read the result. Many applications only set the session cookie after authentication, so a clean scan of the home page proves nothing about it. Consent banners, analytics and load-balancer affinity cookies are what the scanner most often sees, and while those are lower value, a load-balancer cookie without Secure still tells you TLS termination was configured carelessly. Treat the scan as a prompt to inspect the cookies your app sets after login, with the browser's developer tools, using the same three questions: Secure, HttpOnly, SameSite. The wider context for this and the other header-level checks is in HTTP Security Headers Explained and the website security hub; the attack that Secure specifically prevents is described in HSTS Explained, because HSTS and the Secure flag are the two halves of keeping a session off plain HTTP.

Non-session cookies deserve the same treatment

The session cookie gets the attention, but the same reasoning applies to every cookie your application sets. A "remember me" token is a long-lived credential and needs all three flags plus a server-side record that can be revoked. A CSRF token cookie must be Secure and SameSite=Lax, and if you use the double-submit pattern it should be signed so a subdomain cannot plant a matching pair. A preference cookie with no security value still needs Secure, because a cookie sent over HTTP reveals that the user visits your site and can be used to fingerprint them on the network. The only cookie for which SameSite=None is defensible is one that must function inside a third-party iframe, and that cookie should carry nothing beyond what the embed needs.

For a user-side view of the same mechanisms, Cookies Explained covers first- and third-party cookies and what the browser does with each.