模拟实现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 中的场景

验证码

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

优缺点

优点 缺点
安全性高 操作较繁琐,对用户不太友好
相关推荐
挣扎与觉醒中的技术人19 分钟前
【技术干货】三大常见网络攻击类型详解:DDoS/XSS/中间人攻击,原理、危害及防御方案
前端·网络·ddos·xss
zeijiershuai23 分钟前
Vue框架
前端·javascript·vue.js
写完这行代码打球去25 分钟前
没有与此调用匹配的重载
前端·javascript·vue.js
华科云商xiao徐25 分钟前
使用CPR库编写的爬虫程序
前端
狂炫一碗大米饭28 分钟前
Event Loop事件循环机制,那是什么事件?又是怎么循环呢?
前端·javascript·面试
IT、木易29 分钟前
大白话Vue Router 中路由守卫(全局守卫、路由独享守卫、组件内守卫)的种类及应用场景
前端·javascript·vue.js
顾林海30 分钟前
JavaScript 变量与常量全面解析
前端·javascript
程序员小续30 分钟前
React 组件库:跨版本兼容的解决方案!
前端·react.js·面试
乐坏小陈31 分钟前
2025 年你希望用到的现代 JavaScript 模式 【转载】
前端·javascript
生在地上要上天31 分钟前
从600行"状态地狱"到可维护策略模式:一次列表操作限制重构实践
前端