CSRF的攻击和防御

280 阅读4分钟

CSRF(Cross-site request forgery)跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常也被称为 CSRF 或者 XSRF。

CSRF 攻击

攻击原理

攻击者诱导被害者进入第三方网站,在第三方网站中利用被害者已经获得了登录凭证或授权信息,冒充被害者向目标网站发送请求,从而达到攻击。此过程不需要获取用户得登录凭证或者是授权信息,而是通过浏览器的特性。

攻击步骤

  • 用户 A 访问目标网站 B,登录成功
  • 在登录凭证未失效的情况下访问具有攻击性的网站 C
  • C 网站向目标网站 B 发送请求,浏览器会默认带上 B 网站的 Cookie
  • 后端接收到请求,误以为是用户 A 在 B 网站发送的请求,正常返回
  • 攻击完成

场景

目标网站 B

地址 http://localhost:3000
目标网站 B 的首页

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>目标网站</title>
</head>
<body>
  <h3>首页</h3>
  {{if user}}
    <p>你好,{{ user.username }}</p>
  {{/if}}
  {{if !user}}
  <div>
    <a href="/login">登录</a>
  </div>
  {{/if}}
</body>
</html>

目标网站 B 的登录页

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>登录页面</title>
</head>
<body>
  <form action="/login" method="POST">
    <div>
      <label for="">用户名:</label>
      <input type="text" name="username">
    </div>
    <div>
      <label for="">密码:</label>
      <input type="text" name="password">
    </div>
    <div>
      <button>登录</button>
    </div>
  </form>
</body>
</html>

目标网站 B 的后端服务器

const express = require('express')
const session = require('express-session')
const app = express()

app.engine('html', require('express-art-template'))

// 解析表达请求体数据
app.use(express.urlencoded({
  extended: true
}))

// 设置 Session
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true
}))

// CSRF
// 首页
app.get('/', (req, res) => {
  res.render('index.html', {
    user: req.session.user
  })
})

// 登录页
app.get('/login', (req, res) => {
  res.render('login.html')
})

// 登录
app.post('/login', (req, res) => {
  const user = req.body
  console.log(user)
  if (user && user.username === 'yzh' && user.password === '123456') {
    // 登录成功
    // 1.记录登录状态
    // 2.跳转到首页
    req.session.user = user
    return res.redirect('/')
  }
  res.status(401).send('失败了')
})

// 评论
app.post('/comment', (req, res) => {
  if (!req.session.user) {
    return res.status(401).send('未授权')
  }
  res.send('评论成功')
})

app.listen(3000, () => {
  console.log('服务器启动成功:http://localhost:3000')
})

第三方网站 C

地址 http://localhost:5000

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <form action="http://localhost:3000/comment" method="post">
    <input type="text" name="username" hidden>
    <input type="text" name="password" hidden>
    <input type="text" name="account" hidden>
  </form>

  <script>
    document.forms[0].submit()
  </script>
</body>
</html>

攻击步骤

  • 用户 A 登录 目标网站 B
  • 登录凭证未过期
  • 访问第三方网站 C
  • 第三方网站 C 向目标网站 B 发送请求(默认携带 B 网站的 Cookie)
  • 攻击完成

CSRF 攻击分类

GET 请求

<img src="http://example/comment?id=998&content=xxx" />

被害者访问的含有上述 img 的网站,网站会自动加载该图片,然后发送一个 http://example/comment?id=998&content=xxx 链接的 GET 请求,example 服务器就会接收到一个跨域的 GET 请求。

POST 请求

<form action="http://localhost:3000/comment" method="post">
  <input type="text" name="username" hidden>
  <input type="text" name="password" hidden>
  <input type="text" name="account" hidden>
</form>

<script>
  document.forms[0].submit()
</script>

被害者访问了含上述表单的网站,网站会自动发送一个 POST 请求。

链接类型

<a href="http://example/comment?id=998&content=xxx">送500元现金了!!!先到先得</a>

此类型和上面的 GET 请求原理一样,只是一个是用户主动触发,一个是被动触发。当被害者点击该链接就会发送一个跨域请求。

攻击特点:

1、一般发生在第三方
2、攻击者无法获取 Cookie,只能使用

CSRF 防御

阻止不明域名的请求

  • 同源检测
  • SameSite

提交时要求附加本域才能获取的信息

  • CSRF Token
  • 双重 Cookie 验证

同源检测

主要是通过检测,过滤第三方域名的请求。
那我们如何来检测过滤第三方的请求呢?
在 HTTP 请求时,请求头都会携带 OriginReferer 标识来识别源地址。
后端服务器可以通过检测这两个标识进行过滤。

SameSite

SameSiteSet-Cookie 的属性,允许你声明 Cookie 是否应限于第一方或同一站点上下文。说白就是第三方发起请求时是否能访问到你的 Cookie。 分别有三个属性值:

  • Strict
  • Lax
  • None
Strict

最为严格,请求时完全禁止第三方发送 Cookie,只有当前请求 URL 和目标网站一致才会发送 Cookie

Set-Cookie: CookieName=CookieValue; SameSite=Strict;
Lax

Chrome 浏览器默认,相较于 Strict 限制有所放松,只有在导航到目标地址时发送的 GET 请求才会带上 Cookie,其他情况下基本不会发送。

Set-Cookie: CookieName=CookieValue; SameSite=Lax;
请求类型实例正常情况Lax
链接<a href="xxx"></a>发送 Cookie发送 Cookie
预加载<link rel="prerender" href="xxx" />发送 Cookie发送 Cookie
GET 表单<form method="GET" action="xxx"发送 Cookie发送 Cookie
POST 表单<form method="POST" action="xxx">发送 Cookie不发送
iframe<iframe src="xxx"></iframe>发送 Cookie不发送
ajax$.get("xxx")发送 Cookie不发送
Image<img src="xxx" />发送 Cookie不发送

在用户浏览器支持 SameSite 时,设置 StrictLax 后,基本能防御 CSRF 攻击

None

不设置 SameSite 属性,不过必须得和 Secure 一起设置才能生效。

Set-Cookie: widget_session=abc123; SameSite=None; Secure

CSRF Token

前面讲到CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。

而CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开,也可以防范CSRF的攻击。

双重 Cookie 验证

利用 CSRF 攻击不能获取到用户Cookie的特点,我们可以要求 Ajax 和表单请求携带一个 Cookie 中的值。 例如:发送一个 GET 请求,我们可以在请求地址的后面加上 Cookie 的参数,csrfCookie 的值等于 cookie 的值。

$.get("http://example.com/api/test?csrfCookie=xxxx")

后端服务器接收到请求后,判断 csrfCookie 和 Cookie 的值是否一致,如果不一致则直接拒绝。

由于任何跨域都会导致前端无法获取Cookie中的字段(包括子域名之间)
所以会存在用户访问的网站为 www.a.com ,而后端的 api 域名为 api.a.com。那么在 www.a.com 下,前端拿不到 api.a.com 的Cookie,也就无法完成双重Cookie认证。