CSRF防护Skill csrf-protection

跨站请求伪造(CSRF)防护技能,包含同步器令牌、双重提交Cookie、SameSite Cookie属性、自定义头部等技术,用于确保Web应用安全。

安全运维 0 次安装 0 次浏览 更新于 3/3/2026

CSRF防护

概览

使用同步器令牌、双重提交Cookie、SameSite Cookie属性和自定义头部实现全面的跨站请求伪造保护。用于构建表单和状态更改操作时。

何时使用

  • 表单提交
  • 状态更改操作
  • 认证系统
  • 支付处理
  • 账户管理
  • 任何POST/PUT/DELETE请求

实施示例

1. Node.js/Express CSRF防护

// csrf-protection.js
const crypto = require('crypto');
const csrf = require('csurf');

class CSRFProtection {
  constructor() {
    this.tokens = new Map();
    this.tokenExpiry = 3600000; // 1小时
  }

  /**
   * 生成CSRF令牌
   */
  generateToken() {
    return crypto.randomBytes(32).toString('hex');
  }

  /**
   * 为会话创建令牌
   */
  createToken(sessionId) {
    const token = this.generateToken();
    const expiry = Date.now() + this.tokenExpiry;

    this.tokens.set(sessionId, {
      token,
      expiry
    });

    return token;
  }

  /**
   * 验证CSRF令牌
   */
  validateToken(sessionId, token) {
    const stored = this.tokens.get(sessionId);

    if (!stored) {
      return false;
    }

    if (Date.now() > stored.expiry) {
      this.tokens.delete(sessionId);
      return false;
    }

    return crypto.timingSafeEqual(
      Buffer.from(stored.token),
      Buffer.from(token)
    );
  }

  /**
   * Express中间件
   */
  middleware() {
    return (req, res, next) => {
      // 跳过GET, HEAD, OPTIONS
      if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
        return next();
      }

      const token = req.headers['x-csrf-token'] || req.body._csrf;
      const sessionId = req.session?.id;

      if (!token) {
        return res.status(403).json({
          error: 'csrf_token_missing',
          message: 'CSRF令牌是必需的'
        });
      }

      if (!this.validateToken(sessionId, token)) {
        return res.status(403).json({
          error: 'csrf_token_invalid',
          message: 'CSRF令牌无效或已过期'
        });
      }

      next();
    };
  }
}

// Express设置与csurf包
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');

const app = express();

// 会话配置
app.use(cookieParser());
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 3600000
  }
}));

// CSRF保护中间件
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  }
});

app.use(csrfProtection);

// 提供令牌给模板
app.use((req, res, next) => {
  res.locals.csrfToken = req.csrfToken();
  next();
});

// API端点获取CSRF令牌
app.get('/api/csrf-token', (req, res) => {
  res.json({
    csrfToken: req.csrfToken()
  });
});

// 受保护的路由
app.post('/api/transfer', csrfProtection, (req, res) => {
  const { amount, toAccount } = req.body;

  // 处理转账
  res.json({
    message: '转账成功',
    amount,
    toAccount
  });
});

// CSRF错误处理程序
app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    return res.status(403).json({
      error: 'csrf_error',
      message: 'CSRF令牌无效'
    });
  }

  next(err);
});

module.exports = { CSRFProtection, csrfProtection };

2. 双重提交Cookie模式

// double-submit-csrf.js
const crypto = require('crypto');

class DoubleSubmitCSRF {
  /**
   * 生成CSRF令牌并设置Cookie
   */
  static generateAndSetToken(res) {
    const token = crypto.randomBytes(32).toString('hex');

    // 设置CSRF Cookie
    res.cookie('XSRF-TOKEN', token, {
      httpOnly: false, // 允许JS读取双重提交
      secure: true,
      sameSite: 'strict',
      maxAge: 3600000
    });

    return token;
  }

  /**
   * 中间件验证双重提交
   */
  static middleware() {
    return (req, res, next) => {
      // 跳过GET, HEAD, OPTIONS
      if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
        return next();
      }

      const cookieToken = req.cookies['XSRF-TOKEN'];
      const headerToken = req.headers['x-xsrf-token'];

      if (!cookieToken || !headerToken) {
        return res.status(403).json({
          error: 'csrf_token_missing'
        });
      }

      // 比较令牌(时间安全)
      if (!crypto.timingSafeEqual(
        Buffer.from(cookieToken),
        Buffer.from(headerToken)
      )) {
        return res.status(403).json({
          error: 'csrf_token_mismatch'
        });
      }

      next();
    };
  }
}

// Express设置
const app = express();
const cookieParser = require('cookie-parser');

app.use(cookieParser());
app.use(express.json());

// 登录时生成令牌
app.post('/api/login', async (req, res) => {
  // 认证用户
  const token = DoubleSubmitCSRF.generateAndSetToken(res);

  res.json({
    message: '登录成功',
    csrfToken: token
  });
});

// 受保护的路由
app.use('/api/*', DoubleSubmitCSRF.middleware());

app.post('/api/update-profile', (req, res) => {
  // 更新配置文件
  res.json({ message: '配置文件已更新' });
});

3. Python Flask CSRF防护

# csrf_protection.py
from flask import Flask, session, request, jsonify
from flask_wtf.csrf import CSRFProtect, generate_csrf, validate_csrf
from functools import wraps
import secrets

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['WTF_CSRF_TIME_LIMIT'] = 3600  # 1小时
app.config['WTF_CSRF_SSL_STRICT'] = True

csrf = CSRFProtect(app)

# Cookie配置
app.config.update(
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE='Strict'
)

@app.before_request
def csrf_protect():
    """验证状态更改方法的CSRF令牌"""
    if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
        token = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')

        if not token:
            return jsonify({'error': 'CSRF令牌缺失'}), 403

        try:
            validate_csrf(token)
        except:
            return jsonify({'error': 'CSRF令牌无效'}), 403

@app.route('/api/csrf-token', methods=['GET'])
def get_csrf_token():
    """向客户端提供CSRF令牌"""
    token = generate_csrf()
    return jsonify({'csrfToken': token})

@app.route('/api/transfer', methods=['POST'])
def transfer_funds():
    """受保护的端点"""
    data = request.get_json()

    return jsonify({
        'message': '转账成功',
        'amount': data.get('amount')
    })

# 自定义CSRF装饰器
def require_csrf(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if request.method in ['POST', 'PUT', 'DELETE']:
            token = request.headers.get('X-CSRF-Token')

            if not token:
                return jsonify({'error': 'CSRF令牌所需'}), 403

            try:
                validate_csrf(token)
            except:
                return jsonify({'error': 'CSRF令牌无效'}), 403

        return f(*args, **kwargs)

    return decorated_function

@app.route('/api/sensitive-action', methods=['POST'])
@require_csrf
def sensitive_action():
    return jsonify({'message': '操作完成'})

if __name__ == '__main__':
    app.run(ssl_context='adhoc')

4. 前端CSRF实现

// csrf-client.js
class CSRFClient {
  constructor() {
    this.token = null;
    this.tokenExpiry = null;
  }

  /**
   * 从服务器获取CSRF令牌
   */
  async fetchToken() {
    const response = await fetch('/api/csrf-token', {
      credentials: 'include'
    });

    const data = await response.json();
    this.token = data.csrfToken;
    this.tokenExpiry = Date.now() + 3600000; // 1小时

    return this.token;
  }

  /**
   * 获取有效令牌(如果需要则获取)
   */
  async getToken() {
    if (!this.token || Date.now() > this.tokenExpiry) {
      await this.fetchToken();
    }

    return this.token;
  }

  /**
   * 发起受保护的请求
   */
  async request(url, options = {}) {
    const token = await this.getToken();

    const headers = {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token,
      ...options.headers
    };

    return fetch(url, {
      ...options,
      headers,
      credentials: 'include'
    });
  }

  /**
   * 带CSRF令牌的POST请求
   */
  async post(url, data) {
    return this.request(url, {
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  /**
   * 带CSRF令牌的PUT请求
   */
  async put(url, data) {
    return this.request(url, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  /**
   * 带CSRF令牌的DELETE请求
   */
  async delete(url) {
    return this.request(url, {
      method: 'DELETE'
    });
  }
}

// 使用
const client = new CSRFClient();

async function transferFunds() {
  try {
    const response = await client.post('/api/transfer', {
      amount: 1000,
      toAccount: '123456'
    });

    const result = await response.json();
    console.log('转账成功:', result);
  } catch (error) {
    console.error('转账失败:', error);
  }
}

// React钩子用于CSRF
function useCSRF() {
  const [token, setToken] = React.useState(null);

  React.useEffect(() => {
    async function fetchToken() {
      const response = await fetch('/api/csrf-token');
      const data = await response.json();
      setToken(data.csrfToken);
    }

    fetchToken();
  }, []);

  return token;
}

// React表单中的使用
function TransferForm() {
  const csrfToken = useCSRF();

  const handleSubmit = async (e) => {
    e.preventDefault();

    await fetch('/api/transfer', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
      },
      body: JSON.stringify({
        amount: 1000,
        toAccount: '123456'
      })
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="hidden" name="_csrf" value={csrfToken} />
      {/* 表单字段 */}
      <button type="submit">转账</button>
    </form>
  );
}

5. 起源和Referer验证

// origin-validation.js
function validateOrigin(req, res, next) {
  const allowedOrigins = [
    'https://example.com',
    'https://app.example.com'
  ];

  const origin = req.headers.origin;
  const referer = req.headers.referer;

  // 检查Origin头部
  if (origin && !allowedOrigins.includes(origin)) {
    return res.status(403).json({
      error: 'invalid_origin'
    });
  }

  // 检查Referer头部作为回退
  if (!origin && referer) {
    const refererUrl = new URL(referer);
    if (!allowedOrigins.includes(refererUrl.origin)) {
      return res.status(403).json({
        error: 'invalid_referer'
      });
    }
  }

  next();
}

// 应用于状态更改路由
app.use('/api/*', validateOrigin);

最佳实践

✅ 做

  • 对所有状态更改操作使用CSRF令牌
  • 设置SameSite=Strict在Cookie上
  • 验证Origin/Referer头部
  • 使用安全、随机的令牌
  • 实施令牌过期
  • 仅使用HTTPS
  • 在AJAX请求中包含令牌
  • 测试CSRF保护

❌ 不做

  • 跳过认证请求的CSRF
  • 使用GET进行状态更改
  • 单独信任Origin头部
  • 重用令牌
  • 在localStorage中存储令牌
  • 在没有验证的情况下允许CORS中的凭据

CSRF防护方法

  1. 同步器令牌: 服务器生成的令牌
  2. 双重提交Cookie: Cookie和头部匹配
  3. SameSite Cookies: 浏览器级保护
  4. 自定义头部: X-Requested-With
  5. 起源验证: 检查请求起源

防御层

  • [ ] 实施CSRF令牌
  • [ ] 配置SameSite Cookie
  • [ ] 起源/Referer验证
  • [ ] 自定义请求头部
  • [ ] 令牌过期
  • [ ] 安全Cookie标志
  • [ ] 强制HTTPS

资源