上一篇文章我们讲解了 XSS 攻击案例。本文,我们来谈谈 CSRF
- 跨站请求伪造攻击。
CSRF 是什么
CSRF
是 Hacker
利用用户登录的身份凭证(即伪造),通过在用户不知知情的情况下(即跨过)发送恶意请求和执行未经授权的操作。
CSRF
即Cross-Site Request Forgery
,跨站请求伪造。
我们可以试想这么一个例子:
你将私房钱放在了保险箱里面,保险箱只能通过你的拇指指纹才可以打开。某天,你的妻子端了一杯水给你喝,然后收走了杯,通过杯子的指纹提取了你拇指的指纹,然后打开了你的保险箱,取走了为数不多的小金库...
CSRF 攻击案例
下面,我们来简单演示下 CSRF
攻击,先做点前期准备。
案例的演示环境:
macOS Monterey
- Apple M1
node version
- v14.18.1
Visual Studio Code
及其Live Server
插件
首先,我们添加个 hostname
, 方便测试,当然你可以直接使用 ip
地址测试。
通过 sudo vim /etc/hosts
添加 127.0.0.1 a.example.com
的映射:
我们 demo
的场景是 get
接口转账。在登陆的情况下进行转账攻击。
javascript
const Koa = require('koa');
const Router = require('koa-router');
const views = require('koa-views');
const path = require('path');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const app = new Koa();
const router = new Router();
app.use(
views(path.join(__dirname, 'views'), {
extension: 'ejs'
})
);
app.use(bodyParser());
// session
app.keys = ['secret'];
app.use(session(app));
router.get('/', async (ctx) => {
if(ctx.session.user) {
await ctx.render('index');
} else {
ctx.redirect('/login');
}
});
router.get('/login', async (ctx) => {
await ctx.render('login');
})
router.post('/api/login', async (ctx) => {
const { username, password } = ctx.request.body;
if(username === 'jimmy' && password === '123456') {
ctx.session.user = { username, password };
ctx.body = {
message: 'ok'
}
} else {
ctx.body = {
message: 'error'
}
}
})
// use get method to transfer fund
router.get('/api/fund/transfer', async (ctx) => {
const { account, fund } = ctx.query;
if(ctx.session.user) {
ctx.body = {
message: `Success! Transfer ${fund} to the account ${account}.`
}
} else {
ctx.body = {
message: 'please login.'
}
}
})
app.use(router.routes());
app.listen(3000, () => {
console.log("listening on http://localhost:3000");
})
上面接口 /api/fund/transfer
是使用 get
接口进行转账,当然,我们对登陆的用户才进行操作。当用户没有登陆的时候,会抛出异常。
html
<!-- index.ejs -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSRF - Jimmy - Index</title>
</head>
<body>
<h3 style="text-align: center;">CSRF</h3>
<p>Welcome to be here.</p>
<input placeholder="account number" type="number" id="account" />
<input placeholder="fund" type="number" id="fund" />
<button id="transfer">Transfer</button>
<p id="hint"></p>
<script>
let accountDom = document.getElementById('account');
let fundDom = document.getElementById('fund');
let transferDom = document.getElementById('transfer');
let hintDom = document.getElementById('hint');
transferDom.addEventListener('click', function() {
const params = {
account: accountDom.value,
fund: fundDom.value
}
const queryString = Object.keys(params)
.map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
.join("&");
fetch(`/api/fund/transfer?${queryString}`, {
method: 'GET'
})
.then(response => response.json())
.then(data => {
hintDom.innerText = data.message;
})
})
// attack -> http://localhost:3000/api/fund/transfer?account=123&fund=100
</script>
</body>
</html>
但是,当用户登陆这个系统之后:
html
<!-- login.ejs -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSRF - Jimmy - Login</title>
</head>
<body>
<!-- <h3 style="text-align: center;">CSRF</h3> -->
<div>
<input type="text" placeholder="username" id="username" />
<input type="password" placeholder="password" id="password" />
<button id="submit">Submit</button>
</div>
<script>
let submitBtn = document.getElementById('submit');
let usernameDom = document.getElementById('username');
let passwordDom = document.getElementById('password');
submitBtn.addEventListener('click', function() {
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
'username': usernameDom.value,
'password': passwordDom.value
})
}).then((response) => response.json())
.then((data) => {
if(data.message === 'ok') {
window.location.href = '/'
}
})
})
</script>
</body>
</html>
上图,登陆成功并转账成功。那么我们伪造一个链接,比如:
ini
<a href="http://a.example.com:3000/api/fund/transfer?account=456&fund=10000">领取百万大奖</a>
将其发送到你的邮箱上,然后刚好你登陆了系统,你点击了该链接,那么恭喜你,你中奖了:
不知不觉就向账号 456
转账了 10000
元。
【该图片来源网络,侵删】
CSRF 预防
- 凭证失真:代码设置凭证的有效期;或者人为操作,在不使用系统时候记得退出系统
- 关键步骤二次验证:涉及敏感操作,比如金额转账操作,设置密码输入确认等
- 不允许记住密码:电脑一般不是自己独有,登陆系统的时候,账号密码应该禁止浏览器记住
- etc.