前言
之前面试经常被问到 CSRF, 跨站请求伪造
大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为,进而达到执行危险行为的目的,完成攻击
上面就是面试时,我们通常的回答, 但是到底是不是真是这样呢? 难道这么容易伪造吗?于是我就打算试一下能不能实现
接下来,我们就通过node起两个服务 A服务(端口3000)和B服务(端口4000), 然后通过两个页面 A页面、和B页面模拟一下CSRF。
我们先约定一下 B页面是正常的页面, 起一个 4000 的服务, 然后 A页面为伪造者的网站, 服务为3000
先看B页面的代码, B页面有一个登录,和一个获取数据的按钮, 模拟正常网站,需要登录后才可以获取数据
<body> <div> 正常 页面 B <button onclick="login()">登录</button> <button onclick="getList()">拿数据</button> <ul class="box"></ul> <div class="tip"></div> </div> </body> <script> async function login() { const response = await fetch("http://localhost:4000/login", { method: "POST", }); const res = await response.json(); console.log(res, "writeCookie"); if (res.data === "success") { document.querySelector(".tip").innerHTML = "登录成功, 可以拿数据"; } } async function getList() { const response = await fetch("http://localhost:4000/list", { method: "GET", }); if (response.status === 500) { document.querySelector(".tip").innerHTML = "cookie失效,请先登录!"; document.querySelector(".box").innerHTML = ""; } else { document.querySelector(".tip").innerHTML = ""; const data = await response.json(); let html = ""; data.map((el) => { html += `<div>${el.id} - ${el.name}</div>`; }); document.querySelector(".box").innerHTML = html; } } </script>
在看B页面的服务端代码如下:
const express = require("express"); const app = express(); app.use(express.json()); // json app.use(express.urlencoded({ extends: true })); // x-www-form-urlencoded app.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); // 允许客户端跨域传递的请求头 res.header("Access-Control-Allow-Headers", "Content-Type"); next(); }); app.use(express.static("public")); app.get("/list", (req, res) => { const cookie = req.headers.cookie; if (cookie !== "user=allow") { res.sendStatus("500"); } else { res.json([ { id: 1, name: "zhangsan" }, { id: 2, name: "lisi" }, ]); } }); app.post("/login", (req, res) => { res.cookie("user", "allow", { expires: new Date(Date.now() + 86400 * 1000), }); res.send({ data: "success" }); }); app.post("/delete", (req, res) => { const cookie = req.headers.cookie; if (req.headers.referer !== req.headers.host) { console.log("should ban!"); } if (cookie !== "user=allow") { res.sendStatus("500"); } else { res.json({ data: "delete success", }); } }); app.listen(4000, () => { console.log("sever 4000"); });
B 服务有三个接口, 登录、获取列表、删除。 再触发登录接口的时候,会像浏览器写入cookie, 再删除或者获取列表的时候,都先检测有没有将指定的cookie传回,如果有就认为有权限
然后我们打开 http://localhost:4000/B.html
先看看B页面功能是否都正常
我们看到此时 B 页面功能和接口都是正常的, cookie 也正常进行了设置,每次获取数据的时候,都是会携带cookie到服务端校验的
那么接下来我们就通过A页面,起一个3000端口的服务,来模拟一下跨域情况下,能否完成获取 B服务器数据,调用 B 服务器删除接口的功能
A页面代码
<body> <div> 伪造者页面 A <form action="http://localhost:4000/delete" method="POST"> <input type="hidden" name="account" value="xiaoming" /> </form> <script> // 这行可以放到控制台执行,便于观察效果 // document.forms[0].submit(); </script> </div> <ul class="box"></ul> <div class="tip"></div> </body>
A页面服务端代码
<body> <div> 伪造者页面 A <form action="http://localhost:4000/delete" method="POST"> <input type="hidden" name="account" value="xiaoming" /> </form> <script> // 这行可以放到控制台输入 // document.forms[0].submit(); </script> <script src="http://localhost:4000/list"></script> </div> </body>
于是在我们 访问 http://localhost:3000/A.html
页面的时候发现, 发现list列表确实,请求到了, 控制台输入 document.forms[0].submit()
时发现,确实删除也发送成功了, 是不是说明csrf就成功了呢, 但是其实还不是, 关键的一点是, 我们在B页面设置cookie的时候, domain设置的是 localhost
那么其实在A页面, 发送请求的时候cookie是共享的状态, 真实情况下,肯定不会是这样, 那么为了模拟真实情况, 我们把 http://localhost:3000/A.html
改为 http://127.0.0.1:3000/A.html
, 这时发现,以及无法访问了, 那么这是怎么回事呢, 说好的,cookie 会在获取过登录凭证下, 再次访问时可以携带呢。
于是,想了半天也没有想明白, 难道是浏览器限制严格进行了限制, 限制规避了这个问题? 难道我们背的面试题是错误的?