阿里云主机折上折
  • 微信号
Current Site:Index > The double submit cookie scheme

The double submit cookie scheme

Author:Chuan Chen 阅读数:19125人阅读 分类: 前端安全

What is the Double Submit Cookie Scheme

The Double Submit Cookie Scheme is a technical measure to defend against CSRF (Cross-Site Request Forgery) attacks. Its core idea is to have the client carry two identical tokens when sending a request: one sent automatically via a cookie, and the other sent explicitly via a form field or HTTP header. The server verifies the legitimacy of the request by comparing whether these two values match.

This scheme is effective because although attackers can exploit the user's cookies (due to the same-origin policy), they cannot read the cookie content or set custom HTTP headers in frontend code. When the server requires the two tokens to match, forged requests will be rejected due to the absence of one of the tokens.

Detailed Working Principle

  1. Token Generation: The server generates a random encrypted token when the user session starts.
  2. Token Distribution:
    • Writes the token to a cookie via the Set-Cookie header.
    • Embeds the same token in the page (e.g., in a meta tag or JavaScript variable).
  3. Request Verification:
    • The browser automatically carries the token from the cookie.
    • The frontend code explicitly carries another token (via form field/HTTP header).
  4. Server Comparison: Verifies whether the two tokens match and are not expired.
// Server-side token generation example (Node.js)
const crypto = require('crypto');

function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex');
}

// Express middleware example
app.use((req, res, next) => {
  if (!req.cookies._csrf) {
    const token = generateCSRFToken();
    res.cookie('_csrf', token, { 
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production'
    });
    res.locals.csrfToken = token; // For template use
  }
  next();
});

Implementation Methods

Form Submission Scenario

For traditional form submissions, the CSRF token is typically inserted as a hidden field:

<form action="/transfer" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">
  <input type="text" name="amount">
  <button type="submit">Transfer</button>
</form>

AJAX Request Scenario

Modern frontend applications often need to adapt to AJAX requests, which can be implemented as follows:

// Read CSRF token from cookie
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
}

// Add CSRF header to all AJAX requests
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-TOKEN': getCookie('_csrf')
  },
  body: JSON.stringify({ amount: 1000 })
});

Impact of Same-Origin Policy

The double submit scheme relies on the browser's same-origin policy:

  • Cookies are automatically sent with requests (affected by the SameSite attribute).
  • Attackers cannot read the target site's cookie content.
  • Custom HTTP headers cannot be carried in cross-origin requests.

Security Enhancements

SameSite Cookie Attribute

Combining the SameSite attribute can further enhance security:

res.cookie('_csrf', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict' // or 'lax'
});

Token Binding

Binding tokens to user sessions can prevent token substitution attacks:

// During verification, not only compare values but also verify token ownership
function verifyCSRFToken(req) {
  const cookieToken = req.cookies._csrf;
  const bodyToken = req.body._csrf || req.headers['x-csrftoken'];
  
  if (!cookieToken || !bodyToken) return false;
  if (cookieToken !== bodyToken) return false;
  
  // Optional: Verify the token belongs to the current session
  return validateTokenInSession(req.session.userId, cookieToken);
}

Common Issues and Solutions

Multi-Tab Scenario

When users open multiple tabs, token inconsistency may occur. Solutions include:

  • Using session-level cookies instead of persistent cookies.
  • Generating unique tokens for each form and maintaining a token pool.
// Token pool implementation example
const tokenPool = new Map();

app.post('/submit', (req, res) => {
  const { _csrf } = req.body;
  if (!tokenPool.get(req.sessionID)?.includes(_csrf)) {
    return res.status(403).send('Invalid CSRF token');
  }
  // Remove used token after verification
  tokenPool.set(req.sessionID, 
    tokenPool.get(req.sessionID).filter(t => t !== _csrf));
});

API Compatibility Issues

For scenarios requiring support for both web and native apps:

  • Retain the double submit mechanism for web.
  • Use a signature-based authentication scheme for native apps.
// Conditional CSRF check middleware
app.use((req, res, next) => {
  if (req.path.startsWith('/api/') && 
      req.headers['x-requested-with'] === 'mobile-app') {
    return next(); // Skip CSRF check
  }
  // Normal CSRF verification flow
});

Performance Optimization Practices

Token Caching Strategy

For high-traffic scenarios, the following optimizations can be applied:

  • Use HMAC signatures to reduce server-side storage.
  • Set reasonable token expiration times.
// HMAC signature example
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(userSessionId);
const token = hmac.digest('hex');

// During verification, simply recalculate and compare
function verifyToken(sessionId, token) {
  const hmac = crypto.createHmac('sha256', SECRET_KEY);
  hmac.update(sessionId);
  return hmac.digest('hex') === token;
}

CDN Scenario Adaptation

When static resources are hosted on a CDN, special handling is required:

<!-- Use meta tag to pass the token -->
<meta name="csrf-token" content="{{csrfToken}}">

<script>
// Unified frontend token reading logic
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken;
</script>

Comparison with Other Security Mechanisms

Comparison with Traditional CSRF Token

Feature Traditional Scheme Double Submit Scheme
Token Storage Session/DB Cookie
Frontend Involvement High Medium
Distributed Support Requires shared storage Stateless
Defense Strength Strong Strong

Comparison with Encrypted Token Scheme

Encrypted tokens (e.g., JWT) can also be used for CSRF protection but have different characteristics:

  • Encrypted tokens can contain more information.
  • But they cannot achieve instant revocation.
  • Key rotation issues need to be handled.
// JWT implementation example
const jwt = require('jsonwebtoken');

function generateToken(userId) {
  return jwt.sign({ userId }, SECRET_KEY, { expiresIn: '1h' });
}

function verifyToken(token) {
  try {
    return jwt.verify(token, SECRET_KEY);
  } catch {
    return null;
  }
}

Deployment Considerations

Load Balancing Environment

In a multi-server environment, ensure:

  • All servers use the same key to generate tokens.
  • Clock synchronization is important for time-sensitive tokens.
// Use a shared configuration key
const SECRET_KEY = process.env.CSRF_SECRET || 'default-secret';

// Or fetch from a central configuration service
async function getCSRFSecret() {
  return fetchConfigService('csrf_secret');
}

Logging and Monitoring

It is recommended to log CSRF verification failures for security analysis:

app.post('/submit', (req, res) => {
  if (!verifyCSRFToken(req)) {
    securityLogger.warn('CSRF validation failed', {
      ip: req.ip,
      path: req.path,
      userAgent: req.get('User-Agent')
    });
    return res.status(403).send('Forbidden');
  }
  // Normal processing logic
});

Browser Compatibility Handling

Fallback for Older Browsers

For browsers that do not support the SameSite attribute:

  • Add additional verification parameters.
  • Use postMessage for token synchronization.
// Detect SameSite support
const isSameSiteSupported = () => {
  try {
    document.cookie = 'test=1; SameSite=Lax';
    return document.cookie.includes('test=1');
  } catch {
    return false;
  }
};

if (!isSameSiteSupported()) {
  // Enable traditional verification mode
  window.addEventListener('message', syncCSRFToken);
}

Mobile-Specific Handling

Some mobile browsers handle cookies differently. Recommendations:

  • Add UA detection logic.
  • Use longer token expiration times for mobile.
function isMobileBrowser(ua) {
  return /Mobile|Android|iP(hone|od|ad)/.test(ua);
}

app.use((req, res, next) => {
  const isMobile = isMobileBrowser(req.get('User-Agent'));
  res.cookie('_csrf', token, {
    httpOnly: true,
    secure: true,
    maxAge: isMobile ? 86400000 : 1800000 // 1 day for mobile, 30 minutes for web
  });
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.