阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > CSRF防护

CSRF防护

作者:陈川 阅读数:21512人阅读 分类: Node.js

CSRF攻击原理

CSRF(Cross-Site Request Forgery)跨站请求伪造是一种常见的Web安全威胁。攻击者诱导用户在已认证的Web应用中执行非预期的操作。这种攻击利用了Web应用对用户浏览器的信任机制,当用户登录某个网站后,浏览器会自动携带该网站的cookie信息,攻击者通过构造恶意请求让用户在不知情的情况下执行操作。

典型的CSRF攻击流程:

  1. 用户登录可信网站A,服务器返回包含身份验证信息的cookie
  2. 用户未登出网站A的情况下访问恶意网站B
  3. 网站B包含向网站A发起请求的代码(如自动提交表单)
  4. 浏览器自动携带网站A的cookie发送请求
  5. 网站A服务器认为这是用户的合法请求
// 恶意网站可能构造的自动提交表单
<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>

Node.js中的CSRF防护机制

在Node.js生态中,有多种防护CSRF攻击的方案。最常用的是同步令牌模式(Synchronizer Token Pattern),其核心思想是为每个用户会话生成唯一的令牌,要求每个状态修改请求必须包含该令牌。

Express框架中常用的CSRF防护中间件:

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

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

// 获取CSRF令牌的接口
app.get('/csrf-token', (req, res) => {
  res.json({ token: req.csrfToken() });
});

实现双重提交Cookie方案

双重提交Cookie是另一种有效的CSRF防护方法,它不依赖服务器存储令牌,而是要求客户端同时通过cookie和请求体/头传递相同的随机值。

实现步骤:

  1. 服务器在响应中设置包含随机令牌的cookie
  2. 客户端需要在后续请求中从cookie读取该值并附加到请求中
  3. 服务器验证cookie值和请求中的值是否匹配
// 服务器端实现
app.use((req, res, next) => {
  const csrfToken = crypto.randomBytes(16).toString('hex');
  res.cookie('XSRF-TOKEN', csrfToken, { 
    httpOnly: false, // 允许前端JavaScript读取
    secure: process.env.NODE_ENV === 'production'
  });
  req.csrfToken = csrfToken;
  next();
});

// 验证中间件
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验证失败');
    }
  }
  next();
});

同源策略与CORS配置

正确配置CORS(跨源资源共享)可以辅助防止CSRF攻击。虽然CORS不是专门用于CSRF防护的机制,但合理的配置可以减少攻击面。

Node.js中CORS中间件的安全配置:

const cors = require('cors');

app.use(cors({
  origin: ['https://trusted-domain.com'], // 明确指定允许的源
  methods: ['GET', 'POST', 'OPTIONS'],   // 限制允许的HTTP方法
  allowedHeaders: ['Content-Type', 'X-CSRF-Token'], // 限制允许的头部
  credentials: true, // 允许携带凭据
  maxAge: 86400      // 预检请求缓存时间
}));

内容安全策略(CSP)增强防护

内容安全策略可以限制页面加载的外部资源,间接防止某些CSRF攻击向量。通过限制脚本来源,可以阻止恶意脚本执行。

Node.js中设置CSP头部的示例:

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'"], // 限制表单提交目标
    frameAncestors: ["'none'"] // 防止点击劫持
  }
}));

前端与后端的协同防护

完整的CSRF防护需要前后端协同工作。前端需要正确处理CSRF令牌,确保敏感操作请求包含有效的令牌。

React中的实现示例:

import { useState, useEffect } from 'react';

function TransferForm() {
  const [csrfToken, setCsrfToken] = useState('');
  
  useEffect(() => {
    // 从cookie中获取CSRF令牌
    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
      })
    });
    
    // 处理响应...
  };

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

会话管理与CSRF防护

会话管理策略直接影响CSRF防护的有效性。需要考虑会话过期时间、cookie属性和令牌刷新机制。

安全的会话配置示例:

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', // 严格的同站策略
    maxAge: 24 * 60 * 60 * 1000 // 24小时
  },
  rolling: true // 每次请求刷新会话过期时间
}));

针对API的特殊考虑

纯API服务可能需要不同的CSRF防护策略,特别是当客户端是移动应用或第三方服务时。可以考虑使用基于JWT的防护方案。

JWT CSRF防护实现:

const jwt = require('jsonwebtoken');

// 签发双重用途令牌
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
  });
});

// 验证中间件
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();
  }
});

性能与安全权衡

CSRF防护可能对性能产生影响,特别是在高并发场景下。需要考虑令牌生成和验证的效率优化。

令牌缓存优化示例:

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

// 生成并缓存令牌
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();
});

// 验证中间件优化
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验证失败');
    }
  }
  next();
});

常见漏洞场景分析

即使实施了CSRF防护,某些场景下仍可能存在漏洞,需要特别注意。

  1. JSON API漏洞: 当API接受JSON请求时,如果仅依赖cookie进行CSRF防护可能无效,因为浏览器会自动携带cookie。

    防护方案:

    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' });
      }
      
      // 处理正常请求...
    });
    
  2. 子域名漏洞: 如果主站和子域共享cookie,攻击者可能利用子域发起CSRF攻击。

    解决方案:

    // 设置cookie时明确指定domain和sameSite
    res.cookie('XSRF-TOKEN', token, {
      domain: 'example.com',
      sameSite: 'strict',
      secure: true
    });
    
  3. 登录CSRF: 攻击者可能伪造登录请求,使用户以攻击者账户登录。

    防护措施:

    // 登录表单也需要CSRF防护
    app.get('/login', (req, res) => {
      res.render('login', { csrfToken: req.csrfToken() });
    });
    

自动化测试CSRF防护

确保CSRF防护机制有效需要自动化测试。可以使用Jest和Supertest编写测试用例。

测试示例:

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

describe('CSRF防护测试', () => {
  let agent;
  let csrfToken;
  let cookie;
  
  beforeEach(async () => {
    agent = request.agent(app);
    // 获取CSRF令牌
    const response = await agent.get('/csrf-token');
    csrfToken = response.body.token;
    cookie = response.headers['set-cookie'];
  });
  
  test('缺少CSRF令牌应返回403', async () => {
    const res = await agent
      .post('/protected-route')
      .set('Cookie', cookie)
      .send({ data: 'test' });
    
    expect(res.statusCode).toBe(403);
  });
  
  test('有效CSRF令牌应通过验证', 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('无效CSRF令牌应拒绝请求', 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);
  });
});

与其他安全措施的协同

CSRF防护需要与其他安全措施协同工作,形成纵深防御体系。

  1. 与XSS防护结合: XSS漏洞可能绕过CSRF防护,需要同时防范。

    // 使用helmet设置安全头部
    app.use(helmet());
    
    // 输入过滤
    app.use(express.urlencoded({ extended: true }));
    app.use((req, res, next) => {
      // 简单的XSS过滤
      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. 与速率限制结合: 防止CSRF令牌被暴力破解。

    const rateLimit = require('express-rate-limit');
    
    const csrfLimiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15分钟
      max: 100, // 每个IP最多100次请求
      message: '请求过于频繁'
    });
    
    app.use('/csrf-token', csrfLimiter);
    
  3. 与敏感操作验证结合: 关键操作需要额外验证。

    // 转账前需要验证密码
    app.post('/transfer', (req, res) => {
      if (!verifyPassword(req.user.id, req.body.password)) {
        return res.status(403).send('密码验证失败');
      }
      
      // 执行转账...
    });
    

实际部署注意事项

在生产环境中部署CSRF防护需要考虑更多实际因素。

  1. 负载均衡环境: 多服务器环境下需要确保CSRF令牌同步。

    // 使用Redis存储会话和CSRF令牌
    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 }),
      // 其他配置...
    }));
    
  2. CDN和代理配置: 确保安全头部不被CDN剥离。

    # Nginx配置示例
    location / {
      proxy_pass http://node_server;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_cookie_path / "/; secure; HttpOnly; SameSite=Strict";
    }
    
  3. 混合内容问题: 确保全站HTTPS,避免混合内容降低安全性。

    // 强制HTTPS中间件
    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();
    });
    

框架特定实现细节

不同Node.js框架可能有不同的CSRF防护实现方式。

  1. Express csurf中间件

    const csrf = require('csurf');
    const csrfProtection = csrf({ cookie: true });
    
    // 获取令牌的路由不需要保护
    app.get('/form', csrfProtection, (req, res) => {
      res.render('form', { csrfToken: req.csrfToken() });
    });
    
    // 提交表单的路由需要保护
    app.post('/process', csrfProtection, (req, res) => {
      res.send('表单已提交');
    });
    
  2. Koa实现

    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));
    
    // 添加CSRF中间件
    app.use(new CSRF({
      invalidSessionSecretMessage: 'Invalid session secret',
      invalidSessionSecretStatusCode: 403,
      invalidTokenMessage: 'Invalid CSRF token',
      invalidTokenStatusCode: 403,
      excludedMethods: ['GET', 'HEAD', 'OPTIONS'],
      disableQuery: false
    }));
    
    // 在路由中获取令牌
    app.use(async (ctx, next) => {
      if (ctx.method === 'GET') {
        ctx.state.csrf = ctx.csrf;
      }
      await next();
    });
    
  3. NestJS实现

    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('csrf-token')
    getCsrfToken(@Req() req) {
      return { csrfToken: req.csrfToken() };
    }
    

移动端API的特殊处理

移动应用调用API时,传统的CSRF防护可能需要调整。

  1. 自定义头部方案
    // 中间件验证自定义头部
    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'];
        
        if (!appHeader || !apiKey) {
          return res.status(403).json({ error: '缺少必要的安全头部' });
        }
        
        // 验证API密钥...
      }
      next();
    

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

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

上一篇:加密与哈希

下一篇:XSS防护

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌