边学边译JS工作机制22--CRFS攻击以及7种防范策略

753 阅读11分钟

本系列其他译文请看[JS工作机制 - 小白1991的专栏 - 掘金 (juejin.cn)] (juejin.cn/column/6988… 本文阅读指数:5
本文内容不多,且浅显易懂,非常推荐阅读

概述

跨站请求伪造(CSRF),也被称为one-click attack 或者 session riding。也是一种web网站或应用的恶意攻击。这种攻击方式,是指攻击者替代受害者执行恶意请求。恶意应用传递这种请求的方式有很多,比如特殊处理的图片标签,隐藏的表单,AJAX请求等等,它们都是在用户不知情或者无戒心的情况下起作用。 XSS攻击是利用用户对部分网站的信任,CSRF则是利用网站对部分用户浏览器的信任。

CSRF 攻击机制

执行一个CSRF攻击时,受害者提交了一个恶意请求。这个请求可能导致web应用执行一些危险行为,比如客户端和服务端数据的泄漏,修改会话状态或者操控用户的庄户。

CSRF攻击是针对浏览的[confused deputy attack](迷惑代理攻击)的一个案例。浏览器被攻击者欺骗,发送了一个伪造的请求。

CSRF普遍具有以下特点:

  • 设计依赖用户认证的网站和应用
  • 利用网站对该认证的信任
  • 欺骗用户浏览器,发送一个HTTP请求到目标网站
  • 这个HTTP请求产生了副作用

总览一下CSRF攻击的步骤:

  • 受害者执行了一些可疑行为,比如访问攻击者控制的网页,链接等。
  • 这个行为的后果是代替受害者向攻击者的网站发送了一个请求
  • 如果网站此时有受害者的认证信息,那么这个请求就会被当成是受害者发送的一个合法的请求。

注意,CSRF利用的正是受害者激活的认证信息。

大多数情况下,CSRF攻击不会窃取私人信息,而是会触发一些和用户账户相关的操作,比如改变他们的认证信息或者执行一次购买。强制用户从服务器获取数据对攻击者并没有好处,因为请求的回应攻击者是接受不到的。因此,他们会执行改变数据的请求。

web应用中的session管理是基于cookies的,每次发送请求到服务端,浏览器都会附带上相关的cookie来表明当前用户的身份。即使是在不同的域名下,经常也会这么做。攻击者正是利用了这个漏洞。虽然CRFS经常被说成和session有关,但是它也会出现在其他的场景下---只要应用会自动在请求中添加用户的认证信息

Example

Let’s look at the following example which illustrates a simple “Profile page” on a social network web app: 看一个简单的例子:

<!DOCTYPE HTML>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$.get('htps://example.com/api/profile', function(data) {
    $('#username').val(data.name);
    $('#useremail').val(data.email);
});
</script>
</head>
<body>
<form method='post' action='htps://example.com/api/profile'>
  <fieldset>
    <legend>Your Profile:</legend>
    <label for='username'> Name:</label>
    <input name='username' id='username' type='text'> <br><br>

    <label for='email' > Email:</label>
    <input name='email' id='useremail' type='email'> <br><br>
    
    <button type='submit'> Update </button>
  </fieldset>
</form>
</body>
</html>

这个页面简单的加载了用户的账号数据,然后填充在表单中。如果表单编辑了,数据就会被提交并更新。 服务器接受了提交的数据,只要用户当前是认证过的。 现在看一下执行了CSRF的恶意页面。这个页面是攻击者创建的,并且寄宿在另一个域名下。这个页面的目地是向社交网络的发送一个请求:

<!DOCTYPE HTML> 

<html>                                                       
    <head></head>                                                
    <body>                                                       
        <form method='post' action='htps://example.com/api/profile'> 
            <input type='hidden' name='username' value="The Attacker">   
            <input type='hidden' name='email' value="the@attacker.com">  
         </form>                                                                                                          
        <script>                                                     
         document.forms[0].submit();                                  
        </script>                                                    
     </body>                                                      
 </html>

这个页面包含了一个带有隐藏字段的表单,表单的action指向了同样的profile页面。

一旦用户打开了恶意网站,表单就会自动向服务器提交数据。

此时如果用户还没有认证账户,那么这个表达的危害是很小的,因为服务器会拒绝修改用户的数据。但是如果用户已经登录认证了,那么这个请求就是一个合法请求了,服务器就会去修改数据。

image.png

攻击者主要是伪造了请求,并没有直接读取和窃取用户的cookie。 这个例子很简单,真实的攻击会更复杂更隐蔽。例如,CSRF攻击可以嵌入的iframe中,受害者是完全无感知的

我们有很多办法来降低CSRF攻击的风险:

基于Token预防

This defense is one of the most popular and recommended methods to mitigate CSRF attacks. It can be achieved with two general approaches: 这是降低CSRF攻击最流行和最推荐的一种方式。它主要通过两个方法来达到效果:

  • Stateful — 同步的token模式
  • Stateless — 基于token模式加密或者哈希

有很多库为这些技术提供开箱即用的实现

内置 CSRF 实现

在尝试构建应用之前,如果你使用的框架默认支持CSRF保护,那是最好不过了。但即使是这样,我们依然有责任正确的进行配置,比如秘钥管理和token管理

如果你使用的框架没有内置的CSRF防护机制,那么你需要自己实现它。

我们看一下在Express框架是怎么实现内置CSRF防护的。 Express提供了csurf的中间件,它做的就是CSRF保护。

当然我们不会讨论细节,只看关键部分

先写一个index.js:

const express = require('express');
const bodyParser = require('body-parser');
const csrf = require('csurf')
const cookieParser = require('cookie-parser')
const app = express();
const csrfProtection = csrf({ cookie: true });

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.set('view engine', 'ejs');

app.get('/', csrfProtection, (req, res) => {
  res.render('index', { csrfToken: req.csrfToken() });
});

app.post('/profile', csrfProtection, (req, res, next) => {
  res.send(req.body.name);
});

app.listen(3000);

然后views 文件夹中, 我们添加 index.ejs :

<form action='/profile' method='POST'>
  <input type='hidden' name='_csrf' value='<%= csrfToken  %>'>  
  <label for='name'> Name:</label> 
  <input type='text' name='name'>
  <button type='submit'> Update </button>
</form>

路由 / 将会渲染 index.ejs 模板,同时模板中的csrfToken 变量被赋值为CSRF token。

index.ejs中,csrfToken被赋值到隐藏字段中。当表单提交时,会向被保护的/profile路由发送请求。

发现没有CSRF token,会抛出一个无效CSRF token的异常。

同步 Token 模式

同步 Token 模式允许服务端验证请求,确保他们的来源是合法的。 服务端为每一用户seesion和每一个请求生成一个tokern

当请求同客户端发过来,服务端需要跟用户session中的token对比,验证请求中携带token存在并且有效

大多数应用中,服务器使用HTTPsession用来表明用户的登录状态。在这个例子中,服务端生成了session,并且把session ID 发送到了客户端。这个ID大部分时间保存在客户端cookie中

如果保存了session ID 的Cookie没有很好的被保护起来(比如配置httponly,同站,安全等),那它可能就会被浏览器中打开的其他页面访问到。

每个请求都生成一次会更安全一点,因为攻击者利用token的时间更少了。但这样也可能带来用户体验变差。因为很可能用户点了一个返回按钮,但是页面包含的token已经失效了,服务器无法验证token,请求就不会被通过。

CSRF token 需要有下面的特征:

  • 每一个session都是独一无二的
  • 很难预测 — 一个安全的随机数

没有token,攻击者就无法创建一个有效请求,因此可以降低CSRF攻击。

CSRF token也不能使用cookies传送,因为可能被攻击者截取或者访问到。

也不能使用”GET“请求来传递CSRF token,因为有很多种方式可以泄漏出去。比如浏览器历史,日志文件,推荐标头信息等。

CSRF tokens 可以通过这些方式传递:

  • Hidden fields 表单中使用
  • Headers AJAX 调用时使用

比如表单中这样:

<form action=’/api/payment’ method=’post’>
    <input type=’hidden" name=’CSRFToken’ value=’WfF1szMUHhiokx9AHFply5L2xAOfjRkE’>
</form>

像上面的例子一样,token的值是在服务端生成。

Token加密模式

对web应用模式来说,加密模式显然适配性更好,服务端也不用维护任何状态。

服务器生成token,由用户的sessionID 和时间戳组成。这一对是用来加密的秘钥。一旦token生成,就会返回给客户端。和同步token一样,加密token需要在隐藏字段中或者ajax请求的请求头中。

一旦服务器接收到请求,将会尝试去解密token

如果解密失败,意味着请求遭到了某种侵入,请求就会失效。

如果解密成功,会提取sessionID和时间戳。sessionID跟当前认证用户对比,时间戳跟当前服务器对比看是否超过了预定义的过期时间。 如果session ID 匹配,时间戳也没过期,那么请求被任务是安全的。

SameSite Cookies

关于SameSite的补充,[可以看这篇文章]([Cookie Samesite简析 - 知乎 (zhihu.com)]> (zhuanlan.zhihu.com/p/266282015))

SameSite(同站)是cookie是一个属性,也是用来防止CSRF攻击的。 这个属性允许浏览器来决定跨站请求时要是否要带上cookies,这个属性的值有这些

  • Strict —Cookies只会在当前的上下文中发送,第三方网站的请求不会发送。比如你在页面中点击了一个指向GitHub仓库的连接,那么跳转的时候,请求中不会携带cookies
  • Lax —cookies不会再一些容易出现CRSF攻击的请求方法中出现,比如POST。用户导航到源网站时,是可以携带cookies的,如果浏览器没有明确设置SameSite,这就是默认的cookie值。同样,假如此时你想访问一个GitHub的私人仓库,那么就会携带上你的cookies
  • None- 所有的请求都会携带上cookies,不论是当前源还是跨源请求。不过此时需要一个Secure 标记。

桌面浏览器和大部分手机浏览器都支持SameSite属性。 注意这个属性并不能替代token,他们两者是共存的,给用户增加双保险。

同源验证

这种方式需要两步验证HTTP请求头

  • 验证请求发起的源,这个在 Origin 和 Referer头可以看到
  • 验证目标源

服务器需要验证请求源和目标源是匹配的。如果不匹配,请求被认为是跨源,就会被拒绝。这种验证是非常可靠的,因为这些头部信息是禁止修改的,只有浏览器可以操作它们。

Double Submit Cookie

这是对token方案的一种替代,是一种无状态的方案。

当用户访问web应用时,生成一个高安全等级的伪随机数,设置在cookies中,这个值跟session ID是独立的。

服务器接受的每一个请求,都携带了这个随机数(通过隐藏表单或者请求参数)。如果两次发过来的值在服务端验证是匹配的,那么请求就被认定为合法,否则就会拒绝请求。

自定义请求头

这种方式对应用来说适配性最好,主要是在AJAX请求中处理,也依赖于API终结点。

首先要使用同源策略,它会限制在它的源没,只能使用JS增加自定义头。浏览器默认不允许JS发起带有自定义头信息的跨源请求。

这就要求一个稳定的 [CORS]配置方案,因为从其他源过来的请求会触发CORS的预防检查。

允许你增加一个自定义头部信息,然后再服务端验证它的存在和正确性。 使用AJAX时建议可以这么做,<form>元素还是使用token的好。

Interaction-Based Defense

用户行为是最有效的防止CSRF攻击的机制。常见的有两种方式:

  • 预认证 —,在请求发起之前就强制用户认证
  • CAPTCHA

这些都是对抗CSRF强有力的手段,但是也造成了对用户体验的负面影响。所以他们应该主要应用在一些比较关键的行为上,比如转账,下单等。

预认证防御

当用户还未认证时,在一些页面比如登录页,也可能会发生CSRF攻击。和已验证页面相比,这种攻击对预验证页面造成的影响是不同的

假如有一个电子商务网站,受害者认证了自己。此时攻击者可以使用CSRF攻击,将受害者的账户换成自己的。当受害者输入他们的信息卡信息,攻击者就可以使用受害者的信用卡购物了。

为了避免这种攻击,当用户尚未认证时,你可以创建一个预认证。登录表单必须要包含一个CSRF token,就像我们之前提到的那样

一旦用户认证了,pre-session 就会编程真实的session。