本系列其他译文请看[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页面。
一旦用户打开了恶意网站,表单就会自动向服务器提交数据。
此时如果用户还没有认证账户,那么这个表达的危害是很小的,因为服务器会拒绝修改用户的数据。但是如果用户已经登录认证了,那么这个请求就是一个合法请求了,服务器就会去修改数据。
攻击者主要是伪造了请求,并没有直接读取和窃取用户的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仓库的连接,那么跳转的时候,请求中不会携带cookiesLax
—cookies不会再一些容易出现CRSF攻击的请求方法中出现,比如POST
。用户导航到源网站时,是可以携带cookies的,如果浏览器没有明确设置SameSite,这就是默认的cookie值。同样,假如此时你想访问一个GitHub的私人仓库,那么就会携带上你的cookiesNone
- 所有的请求都会携带上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。