模拟实现csrf攻击及预防

注:本文只用于演示,切勿将代码用于非法目的。

csrf 攻击原理

用户登录网站 A,留下 cookie,并保持登录状态 用户打开钓鱼网站 B 钓鱼网站前端向网站 A 后端发送请求,由于网站 A 没有退出登录,所以请求头自动带上网站 A 的 cookie 虽然由于跨域,上面请求的响应会被浏览器拦截,但是请求已经成功发送到网站 A 的后端了

实现

一个简单的办法就是编写一个 login 接口,服务端设置 cookie

前端

通过 axios 发送 http 请求

javascript 复制代码
async login() {
  const res = await axios.post("/login");
}

后端

为了简便,用 koa 实现

使用 ctx.cookies.set 来在响应头设置 cookiecookie 中添加自定义字段 token

javascript 复制代码
router.post("/login", async (ctx, next) => {
  ctx.cookies.set("token", new Date().getTime());
  ctx.set("csrf-token", csrfTokenStore);
  ctx.body = {
    result: "login ok",
  };
});

2. 编写一个有 CSRF 风险的接口:POST /pay

为了避免预检请求失败而阻止正式请求的发出(chrome 104 以前的浏览器有这个现象,详见:专用网络访问规范),我们采用简单请求来实现 postcontent-typeapplication/x-www-form-urlencoded 类型

前端
javascript 复制代码
function pay() {
  axios.post(
    "/pay",
    {
      name: "phish",
    },
    {
      headers: {
        "content-type": "application/x-www-form-urlencoded",
        "csrf-token": this.token,
      },
    }
  );
}
后端

可以获取到请求头中的 cookie 信息

javascript 复制代码
router.post("/pay", async (ctx, next) => {
  const cookie = ctx.request.headers.cookie;
  const array = cookie.split(";");
  const token = array[array.length - 1];

  ctx.body = {
    result: token,
  };
});

3. 创建一个钓鱼网站,为了简单说明 CSRF 攻击的过程,这里只实现前端部分

前端

显示一个按钮,点击后发送钓鱼请求

为了能在请求头中带上 cookieaxios 设置 withCredentials 为 true

根据正常网站的接口设置请求的 content-typeapplication/x-www-form-urlencoded

html 复制代码
<button @click="submit">攻击</button>
javascript 复制代码
import axios from "axios";

axios.defaults.withCredentials = true;

const submit = () => {
  axios.post(
    "http://localhost:8080/pay",
    {
      name: "phish",
    },
    {
      headers: { "content-type": "application/x-www-form-urlencoded" },
    }
  );
};

攻击过程

在正常网站登录,设置cookie

打开钓鱼网站

触发钓鱼请求

这是一个简单请求,所以浏览器没有发送 OPTIONS 预检请求

由于正常网站没有设置允许钓鱼网站跨域,所以跨域请求的响应肯定会被浏览器拦截,但是请求确实已经发到正常网站后端了

钓鱼网站发送的请求头中,带上了正常网站的 cookie ,说明csrf攻击成功

防御

检查 origin 和 referer 和网站域名是否相同

后端示例代码

javascript 复制代码
router.post("/pay", async (ctx, next) => {
  const cookie = ctx.request.headers.cookie;
  const array = cookie.split(";");
  const token = array[array.length - 1];
  const csrfToken = ctx.request.headers["csrf-token"];
  const referer = ctx.request.headers.referer;
  console.log(csrfToken);
  if (referer !== "http://localhost:8080") {
    // 新增代码
    ctx.status = 400;
    return;
  }
  ctx.body = {
    result: token,
  };
});

运行结果

优缺点

优点 缺点
实现简单,origin 和 referer 是浏览器添加的请求头,无法通过前端代码修改 把安全性交给各个浏览器厂商来保证,还是存在被篡改的风险

例子

调/login 接口时,后端设置 sameSite 参数,值为 srtict 或者 lax,详细区别见:SameSite

javascript 复制代码
router.post("/login", async (ctx, next) => {
  ctx.cookies.set("token", new Date().getTime(), {
    httpOnly: false,
    sameSite: "strict",
  }); // 增加sameSite参数
  ctx.body = {
    result: "login ok",
  };
});

效果

钓鱼网站发送的请求,请求头没有带上正常网站的 cookie

备注:用 chrome 浏览器,localhost 下的不同端口号还是会带上不同域的 cookie,所以这里正常网站用 http://localhost:8080,钓鱼网站用http://192.168.1.102:8081 演示

优缺点

优点 缺点
前端代码无感知,由浏览器决定是否带上 cookie 对于 ip 相同,端口号不同的域,chrome 还是会带上 cookie,存在安全风险

csrf token

  1. 登录时,接口返回 csrf token(可以放到 cookie 或者响应头字段中)
  2. 前端把 csrf token 保存到变量中
  3. 后续发请求时,在请求头中添加 csrf token
示例 : csrf token 保存在响应头中

登录时,后台在响应头设置 csrf token

javascript 复制代码
router.post("/login", async (ctx, next) => {
  ctx.cookies.set("token", new Date().getTime(), {
    httpOnly: false,
  });
  ctx.set("header-csrf-token", csrfTokenStore); // 这里后端定义好token的值,并缓存下来
  ctx.body = {
    result: "login ok",
  };
});

前端从登录请求的响应头中获取到 csrf token

javascript 复制代码
async login() {
      const res = await axios.post("/login");
      const token = res.headers.get("header-csrf-token");
      console.log("csrf-token from header: ", token);
      this.token = token; // 前端应用中保存csrf token
    },

后续发送请求时,在前端设置请求头,带上 csrf token

javascript 复制代码
pay() {
      axios.post(
        "/pay",
        {
          name: "phish",
        },
        {
          headers: {
            "content-type": "application/x-www-form-urlencoded",
            "header-csrf-token": this.token,
          },
        }
      );

后端校验请求头的 csrf token 是否和后端缓存的 csrf token 一致

javascript 复制代码
router.post("/pay", async (ctx, next) => {
  const cookie = ctx.request.headers.cookie;

  const headerCsrfToken = ctx.request.headers["header-csrf-token"];
  const referer = ctx.request.headers.referer;
  console.log(headerCsrfToken);

  if (headerCsrfToken !== csrfTokenStore) {
    ctx.status = 400;
    return;
  }

  ctx.body = {
    result: headerCsrfToken,
  };
});

验证

钓鱼网站无法获取 csrf token,所以请求失败

备注

本地验证时,如果用 http://localhost:8081/,前端仍然可以通过 document.cookie 拿到 http://localhost:8080 下的 cookie 字段,所以没法验证 csrf token 保存到 cookie 中的场景

验证码

在进行敏感操作前,把验证码通过短信发给用户,以保证一定是用户本人进行操作。

优缺点

优点 缺点
安全性高 操作较繁琐,对用户不太友好
相关推荐
极小狐4 分钟前
极狐GitLab 的合并请求部件能干什么?
运维·git·安全·gitlab·极狐gitlab
五号厂房15 分钟前
仿照AntDesign,实现一个自定义Tab
前端
浏览器爱好者40 分钟前
如何删除Google Chrome中的所有历史记录【一键清除】
前端·chrome
米开朗基杨42 分钟前
Cursor 最强竞争对手来了,专治复杂大项目,免费一个月
前端·后端
Lonwayne43 分钟前
Web服务器技术选型指南:主流方案、核心对比与策略选择
运维·服务器·前端·程序那些事
brzhang1 小时前
效率神器!TmuxAI:一款无痕融入终端的AI助手,让我的开发体验翻倍提升
前端·后端·算法
海底火旺1 小时前
JavaScript 原型链检查:从 `instanceof` 到 `isPrototypeOf` 的演进
前端·javascript·面试
埃兰德欧神1 小时前
Lynx:革新跨端开发,一次编写,多端闪耀
前端·javascript·前端框架
贾公子1 小时前
详解 LeetCode 第 242 题 - 有效的字母组
前端
前端太佬1 小时前
小程序登录与授权功能全解析:从原理到设计的实战指南 (2025年最新规范实践版)
前端·微信·微信小程序