ASVS 5.0 V3.3.1 requires that every cookie has the Secure attribute, and that its name carries the __Host- prefix, or the __Secure- prefix if __Host- does not fit. It is a Level 1 requirement.
Why it matters
Without Secure, a browser will send the cookie over plain HTTP too. On a shared network an attacker can trigger one plain request, for instance by injecting an image or link, and read the session cookie. HSTS reduces this but does not cover the first visit or every subdomain.
The prefixes add a guarantee enforced by the browser. A cookie named __Secure- can only be set from a secure origin with the Secure attribute. A __Host- cookie must also have Path=/ and no Domain attribute, so a sibling subdomain cannot overwrite it.
How to implement it
Set the attribute and rename the cookie.
Express:
res.cookie("__Host-session", token, {
secure: true,
httpOnly: true,
sameSite: "lax",
path: "/",
});
Django, in settings:
SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_PATH = "/"
SESSION_COOKIE_DOMAIN = None
CSRF_COOKIE_NAME = "__Host-csrftoken"
CSRF_COOKIE_SECURE = True
Spring Boot:
server.servlet.session.cookie.name=__Host-JSESSIONID
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
Do not forget cookies set by frameworks, analytics code and your own JavaScript. If a cookie must be shared across subdomains, use __Secure- and set Domain deliberately. Serve everything over HTTPS and add HSTS, since a Secure cookie is useless if the site never uses TLS.
How to test it
Log in and inspect the Set-Cookie headers in browser developer tools or with curl:
curl -si https://example.com/login -d 'user=a&pass=b' | grep -i set-cookie
Each line should include Secure, and the name should start with __Host- or __Secure-. Then request the site over HTTP and confirm that no cookie is sent back. Automated web scans flag missing attributes, and it is worth adding a CI check that fetches key pages and parses the headers.
The cookie inventory
List every cookie your domains set. Session and CSRF cookies are the obvious ones. Then add those set by the load balancer for stickiness, by single sign on libraries, by consent banners, analytics and chat widgets, and by your own front end code. Each one gets a row: name, purpose, Secure, HttpOnly, SameSite, prefix, lifetime, and whether it is shared across subdomains.
Browser developer tools, a proxy, or a crawl through a web scanner will give you the live list. Compare it with what the team believes it sets. The difference is usually the interesting part.
Local development and testing
Secure cookies are not stored over plain HTTP, and __Host- cookies need HTTPS too. Browsers treat localhost as a secure context, so most local work still functions. For staging, use real certificates. Avoid environment switches that turn Secure off, since they tend to reach production.
Rollout without logging everyone out
Renaming a cookie invalidates existing sessions. Plan for it. Accept both the old and the new name for a period, issue only the new one, and remove the old name after the longest session lifetime has passed. Expire the old cookie explicitly with a Set-Cookie that has a past expiry.
Check it in CI
A small test that requests the login page and parses the headers prevents regressions:
import requests
r = requests.post("https://staging.example.com/login", data={"u": "test", "p": "test"})
for header in r.raw.headers.getlist("Set-Cookie"):
assert "Secure" in header, header
assert header.startswith(("__Host-", "__Secure-")), header
Common mistakes
- Setting
Secureonly in production, so tests never see it. - Renaming the cookie but adding a
Domainattribute, which makes the browser drop a__Host-cookie. - Forgetting secondary cookies, such as CSRF or preferences.
- Terminating TLS at a proxy and having the app think the request is plain HTTP, so it omits
Secure.
Try it yourself
A web scan reports cookie attributes among its findings.
Click through it at your own pace, or open it full screen.
Quick check
What extra guarantee does the __Host- prefix give?
The browser only accepts it from a secure origin with Secure set, Path of slash and no Domain, so subdomains cannot overwrite it.
Why can a proxy cause missing Secure flags?
If TLS ends at the proxy, the app sees HTTP. Configure forwarded protocol headers so the app knows the original scheme.