Secure and SameSite are not checks the server performs. They are instructions the user agent follows when it decides whether to attach a cookie to an outgoing request, and the server only sees the result: a Cookie header, or nothing. That asymmetry is the point, and the trap.

Both are cheap to set and both fail quietly: a missing Secure leaks a session to anyone on the path of a plaintext request; a loose SameSite leaves state-changing requests reachable from another origin; a too-strict one breaks the redirect flows that make single sign-on work. The decision is which of those three you would rather debug.

Secure stops the plaintext copy

Secure tells the user agent to attach the cookie only to requests made with the https scheme, so a plaintext request — an http:// link, a redirect issued before HSTS, a mixed-content subresource — carries no session token for a passive observer. MDN notes the exception: localhost is trustworthy, so Secure cookies do travel over http://localhost in development. The fixture shows the rule client-side: a jar storing five cookies sends only three over plaintext http, withholding the two carrying Secure, then attaches all five once the scheme is https.

What Secure does not do matters as much. It is not a storage rule — the browser stores the cookie and sends it over HTTPS later. It is not an integrity rule, and not a substitute for HttpOnly, which keeps document.cookie and XSS payloads from reading the value. It cannot rescue a response already served over plaintext whose Set-Cookie header was rewritten in transit, which is why Secure belongs with HSTS and a full redirect to HTTPS, not instead of them.

SameSite is about the request’s context

SameSite classifies the request, not the cookie. Strict attaches it only to same-site requests; Lax adds cross-site top-level navigations, and only with a safe method: a link click, a document.location assignment or a GET form qualifies; fetch, an img or script subresource, an iframe navigation and any POST, PUT or DELETE do not. None opts out entirely, and browsers require Secure alongside it: without Secure the cookie is rejected outright. Modern browsers use Lax when the attribute is absent, which is why the default changed without applications editing a line.

That default is not identical to setting the attribute: the implicit variant is the permissive one, which also sends the cookie on top-level cross-site POSTs for cookies younger than two minutes. That carve-out is a migration artefact, not a design. Set the attribute explicitly.

The browser is the only enforcer

None of these flags exist on the wire. The origin cannot tell whether a cookie arrived because a browser honoured Lax or because a script attached whatever it liked. The fixture makes that concrete: a raw socket client sends all five cookies over plaintext http and the server accepts them, while a standard-library cookie jar that implements Secure but has no SameSite concept sends the SameSite=Strict cookie on a request it should never have carried. These are controls on browsers, and only on browsers.

So SameSite is a defence-in-depth layer, not a CSRF fix. Server-side checks — a synchroniser token, or at minimum an Origin or Sec-Fetch-Site check on state-changing routes — protect an endpoint when the client is not a browser, or when its heuristics differ from yours. The flags raise the cost of an attack; they do not remove the need to authorise the request on the server.

When it breaks

Under Lax, the hole is the state-changing GET. The cookie rides along on any top-level navigation with a safe method, so a link to /transfer?to=attacker&amount=100 moves the money if the endpoint accepts GET. The fix is not a stricter SameSite — it is refusing to mutate state on a safe method, because Lax is doing exactly what it promises. A GET form reaches the same endpoint and is equally in scope.

Under Strict, the hole is the login loop. Strict withholds the cookie even on the top-level redirect back from an identity provider, so the app sees an unauthenticated request, bounces to the IdP, and lands in the same unauthenticated state. The symptom is an OAuth or SAML round trip that never completes, often only for users arriving from a bookmark or a link in mail. The usual fix is a second, narrowly scoped cookie for the callback path, not a relaxed session cookie for every request.

Check it quickly

Read the flags the server sets, then confirm that the browser, not your client, is the enforcer:

curl -sD - -o /dev/null https://app.example.com/login | grep -i '^set-cookie:'
curl -s -c /tmp/cj -o /dev/null https://app.example.com/login
curl -sv -b /tmp/cj -o /dev/null http://app.example.com/login 2>&1 | grep -i '^> cookie'
curl -s -b /tmp/cj -H 'Sec-Fetch-Site: cross-site' -o /dev/null -w '%{http_code}\n' https://app.example.com/api/transfer

The first line should carry Secure, HttpOnly and SameSite=Lax or Strict on a __Host- name; the third should print nothing, because the client withholds Secure cookies over plaintext. The fourth returns an ordinary status even though the request announces itself as cross-site — the client sent the cookie anyway. Nothing on that hop enforced the flag, so the server still has to authorise the call.

Sources