图说CSRF攻击

一句话解释

CSRF(Cross-Site Request Forgery,跨站请求伪造),是一种网络攻击方式。

攻击者诱使用户 在已认证的网站 上,发送预期外的请求

本质是利用了 cookie 自动携带的特性。

针对 cookie 作为认证信息,且没进行相关防御的网站。

流程图

sequenceDiagram actor user as 用户 participant bank as 银行网站 participant hack as 黑客网站 Note over user,bank: 正常流程,首次访问生成 Cookie user->>bank: 访问银行网站 bank->>user: 返回登录页面 user->>bank: 输入用户名和密码 bank->>user: 登录成功 %% 访问黑客网站%% Note over user,hack: 访问恶意网站 user->>hack: 访问黑客网站 hack ->> user: 返回攻击内容 Note over user,hack: 攻击内容:诱导点击链接/隐藏表单提交/JS静默请求 user->>bank: 点击链接/提交表单/执行JS!!! bank->>user: 意外转账成功!😭😭😭

代码实现

银行网站 https://bank.com

注意,https 协议与 http 协议一样,是没有办法防范 csrf 的。

xml 复制代码
<!DOCTYPE html>
  <html lang="en">

  <head>
  <meta charset="UTF-8">
  <meta
name="viewport"
content="width=, initial-scale=1.0"
  >
  <title>简易的银行系统</title>
  </head>
  <style>
  .hidden {
  display: none;
}
</style>

  <body>
  <!-- 登录界面 -->
  <div id="userLogin">
  <h2>用户登录</h2>
  <div class="login-form">
  <input
type="text"
placeholder="用户名"
value="chp"
id="userName"
  >
  <input
type="password"
id="userPsd"
  >
  </div>
  <button onclick="handleLogin()">登录</button>
  </div>
  <!-- 登录成功后的页面 -->
  <div
id="userContent"
class="hidden"
  >
  <h2>用户登录成功</h2>
  <p>欢迎回来,demochUser!</p>
  <p>您的账户安全级别为:【几乎没有】</p>
  </div>
  <button onclick="handleLogout()">登出</button>
  <input
id="money"
type="number"
value="1000"
placeholder="请输入您要转账的金额"
  />
  <button onclick="transfer()">转账</button>
  </body>
  <script>
  const handleLogin = () => {
  const name = document.getElementById('userName').value;
  const psd = document.getElementById('userPsd').value;
  if (!name || !psd) {
    alert('请输入用户名和密码')
    return false;
  }
  fetch('/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json', // ✅ 必须设置
    },
    body: JSON.stringify({
      name,
      psd
    })
  }).then(res => res.json()).then(res => {
    if (res.code === 200) {
      document.getElementById('userLogin').classList.add('hidden');
      document.getElementById('userContent').classList.remove('hidden');
      return;
    }
    alert(res.msg);
  })
}
const handleLogout = () => {
  fetch('/logout', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json', // ✅ 必须设置
    },
    credentials: 'include'
  }).then(res => res.json()).then(res => {
    if (res.code === 200) {
      document.getElementById('userLogin').classList.remove('hidden');
      document.getElementById('userContent').classList.add('hidden');
      alert('登出成功')
      return;
    }
    alert(res.msg);
  })
}
const transfer = () => {
  const money = document.getElementById('money').value;
  if (!money) {
    alert('请输入金额');
    return false;
  }
  fetch('/transfer', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json', // ✅ 必须设置
    },
    body: JSON.stringify({
      money
    })
  }).then(res => res.json()).then(res => {
    console.log('🍀🍀🍀🍀', res)
    if (res.code === 200) {
      alert('转账成功');
      return;
    } else {
      alert(res.msg)
    }
  })
}
  </script>

  </html>

银行网站服务器

javascript 复制代码
const Koa = require('koa');
const Router = require('@koa/router');
// koa 解析请求体,user=123 => {user: '123'}
const bodyParser = require('koa-bodyparser')
// koa 静态服务器,用于托管 bank.html 静态页面
const serve = require('koa-static');
// koa 维护登录状态,在 koa-session 的实现中,cookie 和 session 是一一对应的。即本地 cookie 改了,就会认证失败。
const session = require('koa-session').default;
// koa 跨域
const cors = require('@koa/cors');
// 配置 https 服务器
const https = require('https');
const fs = require('fs');
const path = require('path');

// 实例化
const app = new Koa();
const router = new Router();

// session 配置
app.keys = ['sercret-bank-key'];
app.use(bodyParser())
// cors 配置
app.use(cors({
  origin: '*',
  credentials: true, // 允许发送 Cookie
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法
  maxAge: 86400,
}))

// 手动设置 cors
app.use(session({
  key: 'koa.sess',
  maxAge: 86400000, // 1天,ms
  httpOnly: true, // 防止 xss 攻击
  secure: true, // 启用 HTTPS
  signed: true, // 防止篡改
  sameSite: 'None', // 允许跨域
}, app))
app.use(serve(path.join(__dirname)))

/**
 * API 实现
 */
router.all('/login', (ctx) => {
  const {
    name,
    psd
  } = ctx.request.body;

  console.log('🍀🍀🍀🍀', name, psd)
  if (name !== 'chp' || psd !== '123') {
    ctx.body = {
      code: 0,
      msg: '密码错误,登录失败'
    }
  } else {
    ctx.session.user = name;
    ctx.body = {
      code: 200,
      msg: '登录成功'
    }
  }
})
router.all('/logout', (ctx) => {
  ctx.session = null;
  // 手动清除 cookie
  ctx.cookies.set('koa.sess', null, { expires: new Date(0) });
  ctx.cookies.set('koa.sess.sig', null, { expires: new Date(0) })
  ctx.body = {
    code: 200,
    msg: '退出登录成功'
  }
})
// 这里不该放 get 过去,否则用户给出一个链接,也能进行转账操作
router.all('/transfer', (ctx) => {
  if (!ctx.session.user) {
    ctx.body = {
      status: 403,
      code: 0,
      msg: '请先登录'
    }
    return;
  }

  const { money } = ctx.request.body;
  ctx.body = {
    status: 200,
    code: 200,
    msg: `转账成功,转走了${money}元`
  }
  console.log('🍀🍀🍀🍀 转账成功,转走了', money);
})


app.use(router.routes()).use(router.allowedMethods());

// 以下没效果了,给 koa-session 提了个 issue,等解决,官方文档骗人!
app.on('session:missed', (err, ctx) => {
  console.log('🍀🍀🍀🍀', 'ctx, sid')
})
app.on('session:invalid', (err, ctx) => {
  console.log('🍀🍀🍀🍀', 'session:missed')
})
app.on('session:expired', (err, ctx) => {
  console.log('🍀🍀🍀🍀', 'session:missed')
})

// https 证书、私钥配置
const options = {
  key: fs.readFileSync('./bank.com+1-key.pem'),
  cert: fs.readFileSync('./bank.com+1.pem')
}
https.createServer(options, app.callback()).listen(8088, () => {
  console.log('🍀🍀🍀🍀', '运行静态页面 + 对应 api 接口')
})

csrf攻击,第三方网站

xml 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta
    name="viewport"
    content="width=, initial-scale=1.0"
  >
  <title>CSRF攻击网页,嵌入 bank.com 的请求</title>
</head>

<body>
  <h1>
    CSRF攻击网页,嵌入 bank.com 的请求
  </h1>
  <form
    action="https://bank.com:8088/transfer"
    method="POST"
  >
    <input
      name="money"
      value="10000"
    >
    <input
      type="submit"
      value="提交"
    >
  </form>
</body>
<script>
  // js 的 csrf 攻击
  fetch('https://bank.com:8088/transfer', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      money: 10000
    }),
    credentials: 'include'
  }).then(res => res.json()).then(res => {
    console.log('🍀🍀🍀🍀', res)
  })
</script>

</html>

csrf服务器

ini 复制代码
const koa = require('koa');
const fs = require('fs');
const serve = require('koa-static')
const https = require('https');

const options = {
  key: fs.readFileSync('./hack.com+1-key.pem'),
  cert: fs.readFileSync('./hack.com+1.pem')
};

const app = new koa();

app.use(serve(__dirname))

https.createServer(options, app.callback()).listen(1111, () => {
  console.log('🍀🍀🍀🍀', '运行')
})

验证效果

打开 switchhost,修改 host 指向,并且使用 mkcert生成 bank.com``hacker.comssl证书

swithhost配置:

复制代码
127.0.0.1 bank.com
127.0.0.1 hacker.com
  • https://bank.com完成登录,设置 cookie
  • 进入 https://hacker.com执行转账
  • 发现转账成功。

csrf 的成功的关键前提,网站使用 cookie 作为鉴权依据,且 cookie 安全配置薄弱。

  1. cookie 的 samesite None或者 Lax 注意,lax 实际上也不安全,它允许攻击者用 <a>https://bank.com?option=transfer</a> 的方式携带 cookie,从而在这次点击中,完成相应攻击,如果 api 开发者允许 get 请求,且该请求为【关键操作】,可能带来巨大损失
  2. 服务器 cors 配置【网站白名单】为 *,应该明确指定受信任域名。(Access-Control-Allow-Origin)
  3. 服务器 cors 配置允许携带 cookie credentials true 。(Access-Control-Allow-Credentials)

关于 CSRF 钓鱼邮件

在企业工作的时候,经常被提到【不安全的邮件,里面的链接不要乱点】。

其中的原因之一就是,防止被 CSRF 攻击

如果关键操作(如【转账】)允许使用 GET 请求),攻击者仍可通过伪造 <a>链接或诱导用户点击恶意页面。

🍀 安全、便捷,两者有时无法共存。

SameSite=Strict,跨站导航(如shop.com跳转到bank.com)不会发送 Cookie

导致用户需要重新登陆。

这是为了彻底防御 CSRF 击,但牺牲了用户体验

防御手段

CSRF 攻击成功的【充分】【必要】条件:

  • SameSite: None或者 SameSite: Lax的同时,关键操作用 GET
  • Access-Control-Allow-Origin: *
  • Access-Control-Allow-Credentials: true

原理:控制浏览器跨站是否携带 Cookie

选项:

  • SameSite=None允许跨站携带 Cookie。此时 secure 必须配置为 true(https)。
  • SameSite=Lax【默认值是这个】允许部分操作携带(a链接GET导航允许)。
  • SameSite=Strict完全禁止跨站携带 Cookie。

万恶之源1

由于一个公司,可能有多个【域名】【站名】。

所以有些公司图方便,就设置成 None 了。

有时就要这么配,那么往下看。

【基本配置】CORS限制好(针对API)

原理:通过Access-Control-Allow-Origin设置好白名单。

但是【内网攻击】没法防范,假设你们内部员工有问题。还是可能被绕过。

发你一个链接,不要乱点。

rust 复制代码
add_header 'Access-Control-Allow-Origin' 'https://trusted.com';
add_header 'Access-Control-Allow-Credentials' 'true';

一般的偷懒配置,导致了 CSRF 。

万恶之源2

Access-Control-Allow-Origin: *

Access-Control-Allow-Credentials: true

有时就要这么配,那么往下看。

关键操作不要用 GET

【提升 CSRF 的成本】

若是关键操作直接用 GET,又允许跨站携带 cookie,且不做任意校验,那攻击起来太简单了。

一个钓鱼邮件足矣。

使用 CSRF token 进行防御

比较主流的方案。你会在以下地方看到

  1. 主流的电商平台(如京东、淘宝):下单、支付等敏感操作必须携带 cookie
  2. 银行系统(网银转账):大多数他们还有【二次校验】【短信验证码】
  3. 社交网站

【为何主流】

  • 简单有效(攻击者无法获取这个 token,受浏览器同源策略影响,无法获取 LocalStorage 的内容(通常会加密存在 localStorage 里)
  • 兼容性强、适配各种框架
  • 无侵入性,不需要额外验证码

【伪代码实现】

登录成功
ini 复制代码
    ctx.session.user = name;
    ctx.session.loginStatus = true;
    ctx.session.csrfToken = Math.random().toString(36).slice(2); // 随机字符串
    ctx.body = {
      code: 200,
      msg: '登录成功',
      csrfToken: ctx.session.csrfToken
    }
转账校验
css 复制代码
  if (!token) {
    ctx.body = {
      code: 0,
      msg: '没有权限标识,请先登录'
    }
    return;
  }

验证 OriginReferer

原理:检查【请求来源】是否来自受信任的网址。

koa检测origin中间件
ini 复制代码
const allowedDomains = ['https://your-trusted-domain.com'];

module.exports = async (ctx, next) => {
  const origin = ctx.get('Origin');
  const referer = ctx.get('Referer');

  // 允许没有 Origin/Referer 的同源请求(如直接访问)
  if (!origin && !referer) {
    await next();
    return;
  }

  // 校验 Origin
  if (origin && !allowedDomains.includes(origin)) {
    ctx.status = 403;
    ctx.body = { error: '非法请求来源 (Origin)' };
    return;
  }

  // 校验 Referer
  if (referer) {
    try {
      const refererDomain = new URL(referer).origin;
      if (!allowedDomains.includes(refererDomain)) {
        ctx.status = 403;
        ctx.body = { error: '非法请求来源 (Referer)' };
        return;
      }
    } catch (e) {
      ctx.status = 403;
      ctx.body = { error: 'Referer 解析失败' };
      return;
    }
  }

  await next();
};

关键操作增加二次验证

最有效的,添加手机验证码认证,我愿称之为永远的神!!防范 99.99%。

  • 密码/短信验证码
  • 人脸识别/生物识别
  • 邮箱确认

即使 cookie 被盗了,仍需客户主动认证。

如果你直接弃用了Cookie

为了防止钥匙被盗,被复制,于是我们干脆不用"钥匙"开门了?

魔高一尺、道高一丈,遵守原则才不会两行泪。

始终防御 XSS

哪怕不使用 Cookie 鉴权,XSS 可以获取 LocalStorage中的 token,需要确保

  • 避免动态插入 html
  • 设置 CSP 请求头,只允许可信范围的 JS 执行。

HTTPS必须

防止 token 被中间人获取

短期有效的 token

token的有效期设置的尽量短(1小时、15分钟等)。

可以增加 refreshToken 来提升用户体验

相关推荐
kyriewen19 分钟前
我用 Codex 重写了同事维护三年的代码,他没说谢谢——而是找了领导
前端·javascript·ai编程
OpenTiny社区31 分钟前
从零开发 AI 聊天页要两周?试试这款 Vue3 垂直对话组件库 TinyRobot,直接开箱即用
前端·vue.js·github
铁皮饭盒1 小时前
S3已成为文件存储标准,阿里/腾讯/华为云都支持,Bun率先原生支持
前端·javascript·后端
Cobyte1 小时前
22.Vue Vapor 组件 props 的实现
前端·javascript·vue.js
lichenyang4531 小时前
从 has.showToast 看 ASCF 的 API 调用链路
前端
张就是我1065922 小时前
DOMPurify 的一个漏洞:你以为 {} 是空的?
前端
疯狂的魔鬼3 小时前
一套 Schema 驱动四视图:记 useCrudSchemas 的设计与实践
前端·javascript·typescript
风骏时光牛马3 小时前
大模型开发工具高频故障与实操问题汇总代码案例大全
前端
没落英雄3 小时前
2. 让 Agent 能读写文件、执行命令 —— LocalShellBackend 实战
前端·人工智能·架构
白雾茫茫丶3 小时前
探索 Nuxt.js 全栈能力:用 Better-Auth 打造类型安全的 RBAC 权限系统
前端·vue.js·nuxt.js