注:本文只用于演示,切勿将代码用于非法目的。
csrf 攻击原理
用户登录网站 A,留下 cookie
,并保持登录状态 用户打开钓鱼网站 B 钓鱼网站前端向网站 A 后端发送请求,由于网站 A 没有退出登录,所以请求头自动带上网站 A 的 cookie
虽然由于跨域,上面请求的响应会被浏览器拦截,但是请求已经成功发送到网站 A 的后端了
实现
1. 我们要模拟一个正常的网站登录,并留下 cookie 的过程
一个简单的办法就是编写一个 login 接口,服务端设置 cookie
前端
通过 axios
发送 http
请求
javascript
async login() {
const res = await axios.post("/login");
}
后端
为了简便,用 koa
实现
使用 ctx.cookies.set
来在响应头设置 cookie
,cookie
中添加自定义字段 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 以前的浏览器有这个现象,详见:专用网络访问规范),我们采用简单请求来实现 post
,content-type
是 application/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 攻击的过程,这里只实现前端部分
前端
显示一个按钮,点击后发送钓鱼请求
为了能在请求头中带上 cookie
,axios
设置 withCredentials
为 true
根据正常网站的接口设置请求的 content-type
为 application/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 是浏览器添加的请求头,无法通过前端代码修改 | 把安全性交给各个浏览器厂商来保证,还是存在被篡改的风险 |
cookie 设置同源策略
例子
调/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
- 登录时,接口返回 csrf token(可以放到 cookie 或者响应头字段中)
- 前端把 csrf token 保存到变量中
- 后续发请求时,在请求头中添加 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 中的场景
验证码
在进行敏感操作前,把验证码通过短信发给用户,以保证一定是用户本人进行操作。
优缺点
优点 | 缺点 |
---|---|
安全性高 | 操作较繁琐,对用户不太友好 |