阿里云主机折上折
  • 微信号
Current Site:Index > CSRF protection

CSRF protection

Author:Chuan Chen 阅读数:21511人阅读 分类: Node.js

CSRF Attack Principle

CSRF (Cross-Site Request Forgery) is a common web security threat where attackers trick users into performing unintended actions on an authenticated web application. This attack exploits the trust mechanism that web applications have in users' browsers. When a user logs into a website, the browser automatically carries the site's cookie information. Attackers construct malicious requests to make users perform actions unknowingly.

Typical CSRF attack flow:

  1. The user logs into a trusted website A, and the server returns a cookie containing authentication information.
  2. Without logging out of website A, the user visits malicious website B.
  3. Website B contains code that initiates a request to website A (e.g., an auto-submitted form).
  4. The browser automatically sends the request with website A's cookie.
  5. Website A's server considers this a legitimate user request.
// Malicious website may construct an auto-submitted form
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="amount" value="10000"/>
  <input type="hidden" name="toAccount" value="attacker"/>
</form>
<script>
  document.forms[0].submit();
</script>

CSRF Protection Mechanisms in Node.js

In the Node.js ecosystem, there are multiple solutions to protect against CSRF attacks. The most commonly used is the Synchronizer Token Pattern, whose core idea is to generate a unique token for each user session, requiring each state-changing request to include this token.

Common CSRF protection middleware in the Express framework:

const express = require('express');
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

const app = express();
app.use(cookieParser());
app.use(csrf({ cookie: true }));

// Interface to get CSRF token
app.get('/csrf-token', (req, res) => {
  res.json({ token: req.csrfToken() });
});

Implementing the Double Submit Cookie Scheme

The double submit cookie is another effective CSRF protection method that does not rely on server-side token storage. Instead, it requires the client to pass the same random value simultaneously via a cookie and the request body/header.

Implementation steps:

  1. The server sets a cookie containing a random token in the response.
  2. The client needs to read this value from the cookie and attach it to subsequent requests.
  3. The server verifies that the cookie value matches the value in the request.
// Server-side implementation
app.use((req, res, next) => {
  const csrfToken = crypto.randomBytes(16).toString('hex');
  res.cookie('XSRF-TOKEN', csrfToken, { 
    httpOnly: false, // Allow frontend JavaScript to read
    secure: process.env.NODE_ENV === 'production'
  });
  req.csrfToken = csrfToken;
  next();
});

// Verification middleware
app.use((req, res, next) => {
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const cookieToken = req.cookies['XSRF-TOKEN'];
    const bodyToken = req.body._csrf || req.headers['x-csrf-token'];
    
    if (!cookieToken || cookieToken !== bodyToken) {
      return res.status(403).send('CSRF token verification failed');
    }
  }
  next();
});

Same-Origin Policy and CORS Configuration

Properly configuring CORS (Cross-Origin Resource Sharing) can help prevent CSRF attacks. Although CORS is not specifically designed for CSRF protection, reasonable configuration can reduce the attack surface.

Secure CORS middleware configuration in Node.js:

const cors = require('cors');

app.use(cors({
  origin: ['https://trusted-domain.com'], // Explicitly specify allowed origins
  methods: ['GET', 'POST', 'OPTIONS'],   // Limit allowed HTTP methods
  allowedHeaders: ['Content-Type', 'X-CSRF-Token'], // Limit allowed headers
  credentials: true, // Allow credentials
  maxAge: 86400      // Preflight request cache time
}));

Content Security Policy (CSP) for Enhanced Protection

Content Security Policy can restrict external resources loaded by a page, indirectly preventing certain CSRF attack vectors. By limiting script sources, malicious script execution can be blocked.

Example of setting CSP headers in Node.js:

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'", "trusted.cdn.com"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:"],
    connectSrc: ["'self'"],
    formAction: ["'self'"], // Restrict form submission targets
    frameAncestors: ["'none'"] // Prevent clickjacking
  }
}));

Frontend and Backend Collaborative Protection

Complete CSRF protection requires collaboration between frontend and backend. The frontend must correctly handle CSRF tokens to ensure sensitive operation requests include valid tokens.

Implementation example in React:

import { useState, useEffect } from 'react';

function TransferForm() {
  const [csrfToken, setCsrfToken] = useState('');
  
  useEffect(() => {
    // Get CSRF token from cookie
    const token = document.cookie
      .split('; ')
      .find(row => row.startsWith('XSRF-TOKEN='))
      ?.split('=')[1];
    
    setCsrfToken(token || '');
  }, []);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    const response = await fetch('/api/transfer', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
      },
      body: JSON.stringify({
        amount: e.target.amount.value,
        toAccount: e.target.account.value,
        _csrf: csrfToken
      })
    });
    
    // Handle response...
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="hidden" name="_csrf" value={csrfToken} />
      <input type="number" name="amount" />
      <input type="text" name="account" />
      <button type="submit">Transfer</button>
    </form>
  );
}

Session Management and CSRF Protection

Session management strategies directly affect the effectiveness of CSRF protection. Considerations include session expiration time, cookie attributes, and token refresh mechanisms.

Example of secure session configuration:

const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'strict', // Strict same-site policy
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  },
  rolling: true // Refresh session expiration time on each request
}));

Special Considerations for APIs

Pure API services may require different CSRF protection strategies, especially when the client is a mobile app or third-party service. A JWT-based protection scheme can be considered.

JWT CSRF protection implementation:

const jwt = require('jsonwebtoken');

// Issue dual-purpose tokens
app.post('/login', (req, res) => {
  const user = authenticate(req.body);
  const csrfToken = crypto.randomBytes(16).toString('hex');
  
  const accessToken = jwt.sign({
    sub: user.id,
    csrf: csrfToken
  }, process.env.JWT_SECRET, { expiresIn: '1h' });
  
  res.json({
    accessToken,
    csrfToken
  });
});

// Verification middleware
app.use((req, res, next) => {
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      const headerToken = req.headers['x-csrf-token'];
      
      if (decoded.csrf !== headerToken) {
        return res.status(403).send('Invalid CSRF token');
      }
      
      req.user = { id: decoded.sub };
      next();
    } catch (err) {
      res.status(401).send('Invalid token');
    }
  } else {
    next();
  }
});

Performance and Security Trade-offs

CSRF protection may impact performance, especially in high-concurrency scenarios. Efficiency optimizations for token generation and verification need to be considered.

Example of token cache optimization:

const nodeCache = require('node-cache');
const csrfCache = new nodeCache({ stdTTL: 3600 });

// Generate and cache tokens
app.use((req, res, next) => {
  if (req.session) {
    const cachedToken = csrfCache.get(req.sessionID);
    const token = cachedToken || crypto.randomBytes(16).toString('hex');
    
    if (!cachedToken) {
      csrfCache.set(req.sessionID, token);
    }
    
    req.csrfToken = token;
    res.locals.csrfToken = token;
  }
  next();
});

// Optimized verification middleware
app.use((req, res, next) => {
  if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
    const sessionToken = csrfCache.get(req.sessionID);
    const requestToken = req.body._csrf || req.headers['x-csrf-token'];
    
    if (!sessionToken || sessionToken !== requestToken) {
      return res.status(403).send('CSRF verification failed');
    }
  }
  next();
});

Common Vulnerability Scenarios

Even with CSRF protection implemented, vulnerabilities may still exist in certain scenarios, requiring special attention.

  1. JSON API Vulnerability: When an API accepts JSON requests, relying solely on cookies for CSRF protection may be ineffective because browsers automatically carry cookies.

    Protection solution:

    app.use(express.json());
    app.post('/api/json-endpoint', (req, res) => {
      const csrfToken = req.headers['x-csrf-token'];
      const cookieToken = req.cookies['XSRF-TOKEN'];
      
      if (!csrfToken || csrfToken !== cookieToken) {
        return res.status(403).json({ error: 'CSRF token required' });
      }
      
      // Process normal request...
    });
    
  2. Subdomain Vulnerability: If the main site and subdomains share cookies, attackers may exploit subdomains to launch CSRF attacks.

    Solution:

    // Explicitly specify domain and sameSite when setting cookies
    res.cookie('XSRF-TOKEN', token, {
      domain: 'example.com',
      sameSite: 'strict',
      secure: true
    });
    
  3. Login CSRF: Attackers may forge login requests to make users log in with the attacker's account.

    Protective measures:

    // Login forms also need CSRF protection
    app.get('/login', (req, res) => {
      res.render('login', { csrfToken: req.csrfToken() });
    });
    

Automated Testing for CSRF Protection

Ensuring the effectiveness of CSRF protection mechanisms requires automated testing. Jest and Supertest can be used to write test cases.

Testing example:

const request = require('supertest');
const app = require('../app');

describe('CSRF Protection Test', () => {
  let agent;
  let csrfToken;
  let cookie;
  
  beforeEach(async () => {
    agent = request.agent(app);
    // Get CSRF token
    const response = await agent.get('/csrf-token');
    csrfToken = response.body.token;
    cookie = response.headers['set-cookie'];
  });
  
  test('Missing CSRF token should return 403', async () => {
    const res = await agent
      .post('/protected-route')
      .set('Cookie', cookie)
      .send({ data: 'test' });
    
    expect(res.statusCode).toBe(403);
  });
  
  test('Valid CSRF token should pass verification', async () => {
    const res = await agent
      .post('/protected-route')
      .set('Cookie', cookie)
      .set('X-CSRF-Token', csrfToken)
      .send({ data: 'test' });
    
    expect(res.statusCode).toBe(200);
  });
  
  test('Invalid CSRF token should reject request', async () => {
    const res = await agent
      .post('/protected-route')
      .set('Cookie', cookie)
      .set('X-CSRF-Token', 'invalid-token')
      .send({ data: 'test' });
    
    expect(res.statusCode).toBe(403);
  });
});

Collaboration with Other Security Measures

CSRF protection needs to work in conjunction with other security measures to form a defense-in-depth system.

  1. Combining with XSS Protection: XSS vulnerabilities may bypass CSRF protection, requiring simultaneous prevention.

    // Use helmet to set security headers
    app.use(helmet());
    
    // Input filtering
    app.use(express.urlencoded({ extended: true }));
    app.use((req, res, next) => {
      // Simple XSS filtering
      for (const key in req.body) {
        if (typeof req.body[key] === 'string') {
          req.body[key] = req.body[key].replace(/</g, '&lt;').replace(/>/g, '&gt;');
        }
      }
      next();
    });
    
  2. Combining with Rate Limiting: Prevent brute-force attacks on CSRF tokens.

    const rateLimit = require('express-rate-limit');
    
    const csrfLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100, // Maximum 100 requests per IP
      message: 'Too many requests'
    });
    
    app.use('/csrf-token', csrfLimiter);
    
  3. Combining with Sensitive Operation Verification: Critical operations require additional verification.

    // Verify password before transferring
    app.post('/transfer', (req, res) => {
      if (!verifyPassword(req.user.id, req.body.password)) {
        return res.status(403).send('Password verification failed');
      }
      
      // Execute transfer...
    });
    

Deployment Considerations in Production

Deploying CSRF protection in production environments requires considering more practical factors.

  1. Load Balancing Environment: Ensure CSRF token synchronization in multi-server environments.

    // Use Redis to store sessions and CSRF tokens
    const redis = require('redis');
    const client = redis.createClient({
      host: process.env.REDIS_HOST,
      password: process.env.REDIS_PASS
    });
    
    app.use(session({
      store: new RedisStore({ client }),
      // Other configurations...
    }));
    
  2. CDN and Proxy Configuration: Ensure security headers are not stripped by CDNs.

    # Nginx configuration example
    location / {
      proxy_pass http://node_server;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_cookie_path / "/; secure; HttpOnly; SameSite=Strict";
    }
    
  3. Mixed Content Issues: Ensure full-site HTTPS to avoid mixed content reducing security.

    // Force HTTPS middleware
    app.use((req, res, next) => {
      if (req.headers['x-forwarded-proto'] !== 'https' && process.env.NODE_ENV === 'production') {
        return res.redirect(`https://${req.hostname}${req.url}`);
      }
      next();
    });
    

Framework-Specific Implementation Details

Different Node.js frameworks may have different CSRF protection implementations.

  1. Express csurf Middleware:

    const csrf = require('csurf');
    const csrfProtection = csrf({ cookie: true });
    
    // Routes to get tokens do not need protection
    app.get('/form', csrfProtection, (req, res) => {
      res.render('form', { csrfToken: req.csrfToken() });
    });
    
    // Form submission routes need protection
    app.post('/process', csrfProtection, (req, res) => {
      res.send('Form submitted');
    });
    
  2. Koa Implementation:

    const Koa = require('koa');
    const CSRF = require('koa-csrf');
    const session = require('koa-session');
    
    const app = new Koa();
    app.keys = ['some secret key'];
    app.use(session(app));
    
    // Add CSRF middleware
    app.use(new CSRF({
      invalidSessionSecretMessage: 'Invalid session secret',
      invalidSessionSecretStatusCode: 403,
      invalidTokenMessage: 'Invalid CSRF token',
      invalidTokenStatusCode: 403,
      excludedMethods: ['GET', 'HEAD', 'OPTIONS'],
      disableQuery: false
    }));
    
    // Get token in routes
    app.use(async (ctx, next) => {
      if (ctx.method === 'GET') {
        ctx.state.csrf = ctx.csrf;
      }
      await next();
    });
    
  3. NestJS Implementation:

    import { Module, MiddlewareConsumer } from '@nestjs/common';
    import * as csurf from 'csurf';
    
    @Module({})
    export class AppModule {
      configure(consumer: MiddlewareConsumer) {
        consumer
          .apply(
            cookieParser(),
            csurf({ cookie: true })
          )
          .forRoutes('*');
      }
    }
    
    // Get token in controller
    @Get('csrf-token')
    getCsrfToken(@Req() req) {
      return { csrfToken: req.csrfToken() };
    }
    

Special Handling for Mobile APIs

When mobile apps call APIs, traditional CSRF protection may need adjustments.

  1. Custom Header Scheme:
    // Middleware to verify custom headers
    app.use((req, res, next) => {
      if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
        const appHeader = req.headers['x-app-version'];
        const apiKey = req.headers['x-api-key'];
        
    

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

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

上一篇:加密与哈希

下一篇:XSS防护

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 ☕.