Complete Cookie Security Guide: HttpOnly, Secure, SameSite & Advanced Protection
If you've ever had a user's session hijacked or experienced CSRF attacks, you know how critical cookie security is. A single misconfigured cookie can compromise your entire application, but getting cookie security right doesn't have to be complicated.
This guide will show you exactly how to configure cookies securely, so you can protect your users without breaking your application's functionality.
Understanding Cookie Security Threats
Before diving into solutions, it's crucial to understand the threats cookies face:
Session Hijacking
Attackers can steal session cookies through:
- XSS (Cross-Site Scripting): Malicious scripts accessing cookies via
document.cookie - Network Interception: Unencrypted cookies transmitted over HTTP
- Man-in-the-Middle Attacks: Intercepting cookies in transit
Cross-Site Request Forgery (CSRF)
Cookies sent automatically with requests can be exploited to perform unauthorized actions on behalf of authenticated users.
Cookie Theft via JavaScript
Without proper protection, cookies are accessible to JavaScript, making them vulnerable to XSS attacks.
Essential Cookie Security Attributes
HttpOnly: The First Line of Defense
The HttpOnly attribute prevents client-side JavaScript from accessing cookies, protecting against XSS attacks.
What it does:
- Hides cookies from
document.cookieAPI - Prevents XSS-based cookie theft
- Only allows server-side access
Implementation:
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
Framework Examples:
// Express.js
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
// Next.js
cookies().set('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
Secure: Encryption in Transit
The Secure attribute ensures cookies are only sent over HTTPS connections.
What it does:
- Prevents cookie transmission over unencrypted HTTP
- Protects against network interception
- Required for
SameSite=Nonein modern browsers
Critical Note: Always use Secure in production environments.
SameSite: CSRF Protection
The SameSite attribute controls when cookies are sent with cross-site requests, providing CSRF protection.
SameSite Values:
SameSite=Strict
- Most Secure: Cookies never sent with cross-site requests
- Use Case: High-security applications, banking, healthcare
- Trade-off: May break legitimate cross-site flows
Set-Cookie: authToken=xyz; HttpOnly; Secure; SameSite=Strict
SameSite=Lax (Recommended Default)
- Balanced Security: Cookies sent with top-level navigation
- Use Case: Most web applications
- Protection: Blocks CSRF from forms and AJAX requests
Set-Cookie: sessionId=abc; HttpOnly; Secure; SameSite=Lax
SameSite=None
- Cross-Site Required: Cookies sent with all cross-site requests
- Use Case: Third-party integrations, embedded widgets
- Requirement: Must be used with
Secureattribute
Set-Cookie: trackingId=123; HttpOnly; Secure; SameSite=None
Advanced Cookie Security Techniques
Cookie Prefixes: Enhanced Security
Modern browsers support cookie prefixes that enforce security attributes:
__Secure- Prefix
- Automatically enforces
Secureattribute - Prevents accidental HTTP transmission
Set-Cookie: __Secure-sessionId=abc123; HttpOnly; SameSite=Lax
__Host- Prefix
- Restricts cookie to exact hostname
- Prevents subdomain attacks
- Automatically enforces
SecureandPath=/
Set-Cookie: __Host-authToken=xyz789; HttpOnly; SameSite=Strict
Partitioned Cookies (CHIPS)
For third-party contexts, use partitioned cookies to prevent cross-site tracking:
Set-Cookie: trackingId=123; HttpOnly; Secure; SameSite=None; Partitioned
Cookie Expiration and Rotation
Short-Lived Tokens
// Short-lived access token (15 minutes)
res.cookie('accessToken', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
// Longer-lived refresh token (7 days)
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
Token Rotation Strategy
// Rotate tokens on each request
app.post('/api/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (isValidRefreshToken(refreshToken)) {
const newAccessToken = generateAccessToken();
const newRefreshToken = generateRefreshToken();
// Set new tokens
res.cookie('accessToken', newAccessToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000
});
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
}
});
Framework-Specific Implementations
Node.js/Express
const express = require('express');
const session = require('express-session');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
maxAge: 24 * 60 * 60 * 1000, // 24 hours
sameSite: 'lax' // CSRF protection
}
}));
Next.js (App Router)
// app/api/auth/login/route.ts
import { cookies } from 'next/headers';
export async function POST(request: Request) {
const { email, password } = await request.json();
// Authenticate user
const user = await authenticateUser(email, password);
if (user) {
const token = generateJWT(user);
cookies().set('authToken', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24, // 24 hours
path: '/'
});
return Response.json({ success: true });
}
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
}
ASP.NET Core
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromHours(24);
options.SlidingExpiration = true;
});
Django
# settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_AGE = 86400 # 24 hours
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
# CSRF protection
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Lax'
Security Best Practices
1. Cookie Classification and Naming
Session Cookies:
Set-Cookie: __Host-sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
Authentication Cookies:
Set-Cookie: __Secure-authToken=xyz789; HttpOnly; Secure; SameSite=Strict; Path=/
Preference Cookies:
Set-Cookie: userPreferences=theme:dark; Secure; SameSite=Lax; Max-Age=31536000
2. Environment-Specific Configuration
const cookieConfig = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? 'lax' : 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
path: '/'
};
3. Cookie Validation and Sanitization
// Validate cookie values
function validateCookieValue(value) {
// Remove dangerous characters
return value.replace(/[<>\"'&]/g, '');
}
// Set validated cookie
res.cookie('userData', validateCookieValue(userData), {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
4. Secure Cookie Deletion
// Properly delete cookies
res.clearCookie('sessionId', {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/'
});
Testing Cookie Security
Manual Testing Checklist
-
HttpOnly Verification:
// Should return empty string console.log(document.cookie); -
Secure Flag Testing:
- Test over HTTP (should not send cookie)
- Test over HTTPS (should send cookie)
-
SameSite Testing:
<!-- Test CSRF protection --> <form action="https://target-site.com/transfer" method="POST"> <input type="hidden" name="amount" value="1000"> <input type="submit" value="Click me"> </form>
Automated Testing
// Jest test for cookie security
describe('Cookie Security', () => {
test('should set secure cookie attributes', async () => {
const response = await request(app)
.post('/login')
.send({ email: '[email protected]', password: 'password' });
const cookies = response.headers['set-cookie'];
const sessionCookie = cookies.find(c => c.startsWith('sessionId'));
expect(sessionCookie).toContain('HttpOnly');
expect(sessionCookie).toContain('Secure');
expect(sessionCookie).toContain('SameSite=Lax');
});
});
Common Cookie Security Mistakes
1. Missing HttpOnly
<!-- VULNERABLE -->
Set-Cookie: sessionId=abc123; Secure; SameSite=Lax
<!-- SECURE -->
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax
2. Using SameSite=None Without Secure
<!-- VULNERABLE -->
Set-Cookie: trackingId=123; SameSite=None
<!-- SECURE -->
Set-Cookie: trackingId=123; Secure; SameSite=None
3. Storing Sensitive Data in Cookies
// VULNERABLE - Don't store sensitive data
res.cookie('userData', JSON.stringify({
password: 'hashedPassword',
creditCard: '1234-5678-9012-3456'
}));
// SECURE - Store only session identifiers
res.cookie('sessionId', generateSecureSessionId());
4. Inconsistent Cookie Configuration
// VULNERABLE - Inconsistent security settings
app.use(session({
cookie: { secure: false } // Different from other cookies
}));
// SECURE - Consistent configuration
const secureCookieConfig = {
httpOnly: true,
secure: true,
sameSite: 'lax'
};
Monitoring and Alerting
Cookie Security Monitoring
// Monitor for insecure cookie usage
app.use((req, res, next) => {
const cookies = req.headers.cookie;
if (cookies && !req.secure) {
// Log potential security issue
console.warn('Insecure cookies detected over HTTP');
}
next();
});
Security Headers Validation
// Validate cookie security headers
function validateCookieSecurity(req, res, next) {
const cookies = req.headers.cookie;
if (cookies) {
// Check for missing security attributes
const insecureCookies = cookies.split(';').filter(cookie =>
!cookie.includes('HttpOnly') ||
!cookie.includes('Secure') ||
!cookie.includes('SameSite')
);
if (insecureCookies.length > 0) {
console.warn('Insecure cookies detected:', insecureCookies);
}
}
next();
}
Compliance Considerations
GDPR Compliance
- Implement cookie consent mechanisms
- Provide clear cookie policies
- Allow users to opt-out of non-essential cookies
PCI DSS Compliance
- Use strong encryption for payment-related cookies
- Implement proper access controls
- Regular security assessments
HIPAA Compliance
- Encrypt all cookies containing PHI
- Implement audit logging for cookie access
- Use secure transmission protocols
Keep your applications secure with continuous monitoring using Barrion's security dashboard to detect cookie security issues and other vulnerabilities in real-time.
Framework examples
Node/Express:
res.cookie('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
})
Next.js (Route Handlers):
import { cookies } from 'next/headers'
cookies().set('sessionId', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
path: '/',
})
ASP.NET Core:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
});
Django:
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'Lax'
Testing
- Browser DevTools → Application → Cookies: verify attributes
- Check
Set-Cookieheaders in responses - Exercise login, logout, and cross‑site flows
Browser nuances
- Safari handles some cross‑site navigation and redirect chains differently. Test login/return flows with
SameSite=Lax, redirects, and multiple tabs/windows. - Mobile webviews can cache aggressively. Ensure logouts clear relevant cookies.
Common mistakes
- Missing
Securein production - Using
SameSite=NonewithoutSecure - Storing JWTs in
localStorageinstead of HttpOnly cookies
Advanced tips
- Keep cookie lifetime short and rotate sessions on privilege changes
- Bind sessions to device hints (IP ranges, User‑Agent) if risk warrants
- Pair with CSRF tokens on state‑changing POST requests
Conclusion
Set cookies with HttpOnly, Secure, and a sensible SameSite by default. Test user flows and third‑party integrations, especially authentication, so that the security changes doesn’t break functionality.
Keep tabs on regressions with ongoing security checks in the Barrion dashboard.