How HTTP Works

Cookie attribute

< Set-Cookie: id=a3fWa; Secure

Secure

Learn how the Secure cookie attribute ensures cookies are only sent over HTTPS connections. Protect sensitive data from man-in-the-middle attacks.

Reviewed 3 min readbeginner2 sourcesTry itMarkdown
Default if omitted
Absent: also sent over plain HTTP
Accepts
Flag only, no value
Sent back to server
No: never echoed in the Cookie header
Spec
RFC 6265 §4.1.2.5, RFC 6265bis (draft)
On this page

TL;DR: Secure tells the browser to send the cookie only over HTTPS and to refuse it from insecure responses. Set it on every cookie of an HTTPS site; if cookies vanish behind a load balancer, the app does not know the original request was HTTPS.

What Secure does and does not do

Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Lax; Path=/
  • The cookie is attached only to requests over HTTPS. A plain http:// request to the same host will not carry it.
  • Modern browsers (Chrome, Firefox, Safari) also refuse to accept a Secure cookie from an insecure origin (“leave secure cookies alone” in the RFC 6265bis storage model), and refuse to let an HTTP response overwrite an existing Secure cookie. This blocks the classic attack where a network attacker injects a cookie via a spoofed HTTP response.
  • It does not encrypt the value in the browser, hide it from JavaScript (use HttpOnly), or restrict which sites can trigger it (use SameSite).
  • Port and path are not part of the check: Secure is about scheme only, so the cookie is also sent to other HTTPS ports on the same host.

Localhost

Chrome, Edge and Firefox treat http://localhost as a secure context, so Secure cookies work in development without a certificate. Safari has historically not accepted Secure cookies from http://localhost, so a login that works in Chrome and silently fails in Safari is a sign to use a local HTTPS certificate (mkcert) or to make the flag conditional on environment. http://127.0.0.1 and custom hosts like app.local are treated differently per browser; do not assume they behave like localhost.

Why Secure cookies disappear behind a proxy

TLS usually terminates at nginx, an AWS ALB, or Cloudflare, and the app sees plain HTTP. Frameworks that set Secure only on HTTPS requests then silently drop it, or refuse to set the cookie at all.

// Express: trust the first proxy so req.secure is true when X-Forwarded-Proto: https
app.set('trust proxy', 1)
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, sameSite: 'lax' }
}))

Without trust proxy, express-session with cookie.secure: true over an HTTP hop does not send Set-Cookie at all. That is the usual cause of “session works locally, new session on every request in production”.

location / {
    proxy_pass http://app;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $host;
}
# Django settings.py
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Flask behind a proxy
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
app.config.update(SESSION_COOKIE_SECURE=True)
// Go net/http
http.SetCookie(w, &http.Cookie{
    Name: "session", Value: token, Path: "/",
    Secure: true, HttpOnly: true, SameSite: http.SameSiteLaxMode,
})

Cloudflare “Flexible” SSL is a related trap: the browser talks HTTPS to Cloudflare but Cloudflare talks HTTP to your origin. Use Full (strict) and forward X-Forwarded-Proto.

Prefixes: __Secure- and __Host-

Cookie name prefixes make the browser enforce attributes at set time:

Set-Cookie: __Secure-id=a3fWa; Secure; Path=/
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; Path=/; SameSite=Lax

__Secure- requires Secure and an HTTPS origin. __Host- additionally requires Path=/ and no Domain attribute, which pins the cookie to exactly one host and stops sibling subdomains from overwriting it. Prefer __Host- for session cookies. See Domain and Path.

Pair it with HSTS

A first visit to http://example.com is unprotected until the redirect to HTTPS. Strict-Transport-Security makes the browser upgrade to HTTPS before sending any cookie, closing that gap.

Verify

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

In Chrome DevTools, Application, Cookies shows the Secure column. In the Network panel, a blocked cookie shows a warning triangle with the reason (for example “This Set-Cookie was blocked because it had the Secure attribute but was not received over a secure connection”).

Frequently asked questions

What is the Secure cookie attribute?

Secure ensures cookies are only sent over HTTPS connections. It prevents cookies from being transmitted over unencrypted HTTP, protecting against interception.

Should all cookies be Secure?

Yes on any site served over HTTPS. Secure stops the cookie from being sent over plain HTTP, and in modern browsers also stops an insecure origin from overwriting it. It does not encrypt the value or hide it from JavaScript; that is HttpOnly.

Can I set Secure cookies on localhost?

Chrome, Edge and Firefox treat http://localhost as a secure context and accept and send Secure cookies there. Safari has historically refused Secure cookies over plain http://localhost, so test with a local HTTPS certificate (for example mkcert) if Safari matters.

What happens if I access HTTP with Secure cookies?

Secure cookies are not sent over HTTP. The server will not receive them, potentially logging users out or breaking functionality. Always use HTTPS.

Sources

  1. MDN Web Docs: Secure Cookiedeveloper.mozilla.org
  2. RFC 6265bis (draft): Secure attribute and cookie name prefixesdatatracker.ietf.org

Keep going

Browse /search