图说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 来提升用户体验

相关推荐
又又呢21 分钟前
前端面试题总结——webpack篇
前端·webpack·node.js
dog shit1 小时前
web第十次课后作业--Mybatis的增删改查
android·前端·mybatis
我有一只臭臭1 小时前
el-tabs 切换时数据不更新的问题
前端·vue.js
七灵微1 小时前
【前端】工具链一本通
前端
Nueuis2 小时前
微信小程序前端面经
前端·微信小程序·小程序
_r0bin_5 小时前
前端面试准备-7
开发语言·前端·javascript·fetch·跨域·class
IT瘾君5 小时前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
potender5 小时前
前端框架Vue
前端·vue.js·前端框架
站在风口的猪11085 小时前
《前端面试题:CSS预处理器(Sass、Less等)》
前端·css·html·less·css3·sass·html5
程序员的世界你不懂6 小时前
(9)-Fiddler抓包-Fiddler如何设置捕获Https会话
前端·https·fiddler