Set Secure, HttpOnly, and SameSite flags on session cookies
All session and authentication cookies are issued with the Secure, HttpOnly, and an appropriate SameSite flag to prevent interception, JavaScript exfiltration, and cross-site request forgery.
- Secure — cookie is only sent over HTTPS, never plain HTTP
- HttpOnly — cookie is invisible to JavaScript (blocks XSS theft)
- SameSite=Strict or Lax — prevents the cookie from being sent on cross-site requests (blocks CSRF)
- Never use SameSite=None without also setting Secure and understanding the CSRF implications
Rule Details
Session cookies are the keys to the kingdom. The three security flags are independent mechanisms that each block a distinct attack vector.
Code Example
Set-Cookie: session_id=abc123xyz;
HttpOnly;
Secure;
SameSite=Strict;
Path=/;
Max-Age=86400Why It Matters
Missing cookie flags are one of the most common and easily fixed authentication weaknesses. Without Secure, session tokens are transmitted in plain text over HTTP and can be captured by network eavesdroppers. Without HttpOnly, any XSS payload can exfiltrate the session token in one line. Without SameSite, any website can trigger authenticated actions on behalf of the victim without their knowledge.
Flag Reference
| Flag | What it prevents | When to omit |
|---|---|---|
Secure | Transmission over plain HTTP | Never (on production) |
HttpOnly | JavaScript access (XSS token theft) | Only for cookies that JS must read (e.g., CSRF token) |
SameSite=Strict | All cross-site requests | When your app uses cross-site navigation or OAuth redirects |
SameSite=Lax | Non-safe cross-site requests (POST, PUT, DELETE) | When you need Strict level protection |
SameSite=None; Secure | (no CSRF protection) | Third-party cookie scenarios (embeds, iframes) |
Server Implementations
Next.js (App Router)
// lib/cookies.ts — reusable cookie options
import type { ResponseCookie } from 'next/dist/compiled/@edge-runtime/cookies'
const isProd = process.env.NODE_ENV === 'production'
export const SESSION_COOKIE_OPTIONS: Partial<ResponseCookie> = {
httpOnly: true,
secure: isProd, // false in dev (localhost has no HTTPS)
sameSite: 'strict',
path: '/',
maxAge: 60 * 60 * 24, // 24 hours
}
export const REFRESH_COOKIE_OPTIONS: Partial<ResponseCookie> = {
httpOnly: true,
secure: isProd,
sameSite: 'strict',
path: '/api/auth', // Restrict to auth endpoints only
maxAge: 60 * 60 * 24 * 7, // 7 days
}// app/api/auth/login/route.ts
import { NextResponse } from 'next/server'
import { SESSION_COOKIE_OPTIONS } from '@/lib/cookies'
export async function POST(request: Request) {
// ... authenticate user ...
const response = NextResponse.json({ ok: true })
response.cookies.set('session_id', sessionToken, SESSION_COOKIE_OPTIONS)
return response
}Express.js
import express from 'express'
import session from 'express-session'
const app = express()
app.use(
session({
secret: process.env.SESSION_SECRET!,
name: '__session', // Rename from default 'connect.sid'
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000, // 24 hours in milliseconds
},
})
)Node.js (raw Set-Cookie header)
import { createServer, type IncomingMessage, type ServerResponse } from 'http'
function setSecureCookie(
res: ServerResponse,
name: string,
value: string,
maxAgeSeconds = 86400
) {
const flags = [
`${name}=${value}`,
`Max-Age=${maxAgeSeconds}`,
'Path=/',
'HttpOnly',
'Secure',
'SameSite=Strict',
]
res.setHeader('Set-Cookie', flags.join('; '))
}Nginx (for session cookies set by upstream)
# Ensure the upstream's Set-Cookie has the right flags
# (Better to set flags in the application, but this adds a safety net)
proxy_cookie_flags ~ httponly secure samesite=strict;SameSite Behaviour Details
SameSite=Strict:
✅ Same-site GET, POST, etc.
❌ Cross-site navigation (typing URL, clicking link from another site)
❌ Cross-site POST, DELETE, PUT, PATCH
SameSite=Lax (browser default in modern browsers):
✅ Same-site requests
✅ Top-level cross-site GET navigation (clicking a link)
❌ Cross-site POST, DELETE, PUT, PATCH (subresource requests)
SameSite=None:
✅ All requests (same-site and cross-site)
⚠️ Requires Secure; provides NO CSRF protectionWhen to use Lax instead of Strict
Use SameSite=Lax when your authentication flow involves cross-site redirects — for example, OAuth 2.0 where the user is redirected from a third-party identity provider back to your site. Strict would drop the session cookie on that final redirect, logging the user out immediately after they authenticate.
// For OAuth callback routes, use Lax to survive the cross-site redirect
export const OAUTH_STATE_COOKIE_OPTIONS: Partial<ResponseCookie> = {
httpOnly: true,
secure: true,
sameSite: 'lax', // Survives the redirect from the identity provider
maxAge: 60 * 10, // 10-minute PKCE state lifetime
path: '/api/auth',
}Cookie Prefixes
Cookie prefixes are a browser mechanism that enforces security constraints by naming convention:
__Host-session_id → Forces Secure, forbids Domain, requires Path=/
__Secure-session_id → Forces Secure// Using the __Host- prefix for maximum security
response.cookies.set('__Host-session', sessionToken, {
httpOnly: true,
secure: true, // Required by the prefix
sameSite: 'strict',
path: '/', // Required by __Host-
// domain: must NOT be set (required by __Host-)
})Auditing Existing Cookies
# Check cookie flags via curl
curl -I -s https://example.com/api/login -d '{"email":"test"}' \
-H 'Content-Type: application/json' | grep -i set-cookie
# Expected output:
# set-cookie: session_id=...; Path=/; HttpOnly; Secure; SameSite=StrictBrowsers without SameSite support treat the absence of the flag as None (sending cookies on all cross-site requests). Modern browsers default to Lax, but you should always set SameSite explicitly rather than relying on browser defaults, which change across versions.
Exceptions
- Client-side storage for non-sensitive UI state is not equivalent to storing credentials, session identifiers, or long-lived secrets.
- Framework defaults are not exceptions by themselves; only documented constraints with compensating controls should suppress the finding.
- When several storage protections fail together, prioritize the control that most directly prevents credential theft or replay.
Standards
- Align the implementation with MDN: Set-Cookie and verify the effective response or browser behavior, not only the configuration file.
- Align the implementation with OWASP: Session Management Cheat Sheet and verify the effective response or browser behavior, not only the configuration file.
Verification
Automated Checks
- Run
curl -Ion the login endpoint and verify theSet-Cookieheader includes all three flags. - Use Mozilla Observatory (opens in new tab) to scan the site and verify the cookie security score.
Manual Checks
- Log in to the application, then open DevTools → Application → Cookies and confirm session cookies show the
HttpOnlyandSecurecheckboxes ticked andSameSiteset toStrictorLax. - Check that session cookies are absent from
document.cookieoutput in the browser console.
Use with AI
Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.
Check
Verify implementation
Check whether session and authentication cookies are set with the Secure, HttpOnly, and SameSite flags.
Fix
Auto-fix issues
Update the server's cookie configuration to include Secure, HttpOnly, and SameSite=Strict (or Lax) on all session and auth cookies.
Explain
Learn more
Explain what each cookie security flag does and the specific attack each one prevents.
Review
Code review
Review all Set-Cookie headers and cookie creation code. Flag any cookies missing the HttpOnly flag, absent Secure flag, or an unspecified or overly permissive SameSite setting.