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

验证码

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

优缺点

优点 缺点
安全性高 操作较繁琐,对用户不太友好
相关推荐
Fireworkitte34 分钟前
gRPC和http长轮询
网络·网络协议·http
brzhang4 小时前
我操,终于有人把 AI 大佬们 PUA 程序员的套路给讲明白了!
前端·后端·架构
止观止5 小时前
React虚拟DOM的进化之路
前端·react.js·前端框架·reactjs·react
goms5 小时前
前端项目集成lint-staged
前端·vue·lint-staged
谢尔登5 小时前
【React Natve】NetworkError 和 TouchableOpacity 组件
前端·react.js·前端框架
Lin Hsüeh-ch'in5 小时前
如何彻底禁用 Chrome 自动更新
前端·chrome
Immortal__y5 小时前
网络安全初级--搭建
安全·web安全
Rvelamen6 小时前
LLM-SECURITY-PROMPTS大模型提示词攻击测评基准
人工智能·python·安全
凯基迪科技7 小时前
游戏设备软件加密锁复制:技术壁垒与安全博弈
安全·游戏
augenstern4167 小时前
HTML面试题
前端·html