CSRF 攻击案例

上一篇文章我们讲解了 XSS 攻击案例。本文,我们来谈谈 CSRF - 跨站请求伪造攻击。

CSRF 是什么

CSRFHacker 利用用户登录的身份凭证(即伪造),通过在用户不知知情的情况下(即跨过)发送恶意请求和执行未经授权的操作。

CSRFCross-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.

参考

相关推荐
小政爱学习!11 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。16 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼22 分钟前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k093326 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架
web行路人1 小时前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0011 小时前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼9211 小时前
【Ajax】跨域
javascript·ajax·cors·jsonp