个人博客
CSRF(Cross-site Request Forgery),跨站请求伪造攻击,简单来说就是攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送请求,并利用受害者在被攻击网站中获取的用户凭证,达到冒充受害者的目的,并使用受害者的身份进行一些恶意操作。
一个典型的 CSRF 攻击过程如下:
比如可以使用图片的 src 属性来发送一个 GET 请求:
<img src="https://www.google.com" style="display: none;">
![](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2018b/ff0cdbee.example/withdraw?amount=10000&for=hacker)
受害者访问这个页面后,会自动发起一次 GET 请求,且用户没有任何感觉。
这种类型的攻击一般会是一个自动提交的表单,如:
<form action="http://bank.example/withdraw" method=POST> <input type="hidden" name="account" value="xiaoming" /> <input type="hidden" name="amount" value="10000" /> <input type="hidden" name="for" value="hacker" /> </form> <script> document.forms[0].submit(); </script>
当访问该页面时,表单会自动提交,相当于模拟用户完成了一次 POST 操作。
链接类型的 CSRF 攻击相较于以上两种自动完成的 CSRF 攻击来说不是那么容易中招,这种情况需要用户点击链接后才会触发。
这种类型通常会是论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户点击,如:
<a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank"> 重磅消息!! <a/>
由于 CSRF 一般是在第三方网站发起的,因此被攻击网站无法阻止攻击发生,只能对自己的网站进行防护。
那么由于 CSRF 一般来自于第三方网站,因此我们只要禁止外域(或不受信任的域)的请求即可。
而判断是否是外域可以使用请求头部字段中的 Origin
或 Referer
字段或是 Host
字段,这几个字段都会被服务器自动加在请求头中。
Host:域名 + 端口号(若无则使用默认端口),指明了请求将要发送到的服务器主机名和端口号。
host 字段在 http/1.1 请求头中必须包含。若缺少了 host 字段或多于一个,则可能会收到 400(bad request) 状态码。
例:
Host: developer.mozilla.org
Origin:协议 + 域名 + 端口。指示了请求来自于哪个站点,且不包含任何路径信息。这个字段用于 CORS 请求或 POST 请求。
只有跨域请求,或者同域时发送 post 请求,才会携带 origin 请求头。 如果浏览器不能获取请求源,那么 origin 满足上面情况也会携带,不过其值为 null。
例:
Origin: https://developer.mozilla.org
Referer:协议 + 域名 + 端口 + 路径 + 参数(完整的 url 路径)。包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。除了多一个路径外,其他和 Origin 是差不多的。
在以下几种情况下,Referer 不会被发送:
- 来源页面采用的协议为表示本地文件的 “file” 或者 “data” URI;
- 当前请求页面采用的是非安全协议,而来源页面采用的是安全协议(HTTPS);
- 直接输入网址或通过浏览器书签访问;
- 使用 JavaScript 的 Location.href 或者是 Location.replace();
- 使用 html5 中 noreferrer
例:
Referer: https://developer.mozilla.org/en-US/docs/Web/JavaScript
MDN
2014年,W3C的Web应用安全工作组发布了Referrer Policy草案,对浏览器该如何发送Referer做了详细的规定。
Referrer-Policy: no-referrer Referrer-Policy: no-referrer-when-downgrade Referrer-Policy: origin Referrer-Policy: origin-when-cross-origin Referrer-Policy: same-origin Referrer-Policy: strict-origin Referrer-Policy: strict-origin-when-cross-origin Referrer-Policy: unsafe-url
no-referer
:不携带 Referer 字段
no-referer-when-downgrade
:默认值,在同等安全级别下,会携带 Referer 字段,而在安全级别下降的情况下(HTTPS -> HTTP)则不会。
origin
:任何情况下,Referer 字段的值只包含发送源(即不包含路径,相当于 Origin)
origin-when-cross-origin
:对同源的请求会发送完整的 URL,而非同源请求则仅发送源
same-origin
:同源请求则发送 Referer 字段,非同源则不发送
strict-origin
:同 no-referer-when-downgrade,区别是这边只发送源(不包含路径)
strict-origin-when-cross-origin
:同源请求会发送完整的 URL,同等安全级别下,发送源,降级情况下则不会发送
unsafe-url
:无论是同源还是非同源都会发送完整的 URL(移除参数后)
使用:mate
标签可以为整个文档指定 referer 策略:
<meta name="referrer" content="origin">
或是在一些标签如 a
、area
、img
、iframe
、script
、link
上指定 referrerpolicy
属性(注意双写 r)
<a href="http://example.com" referrerpolicy="origin">
或是在 a
、area
、link
上将 rel
属性指定为 noreferrer
<a href="http://example.com" rel="noreferrer">
CSRF 攻击仅仅是冒用 Cookie 信息,而无法直接获取到用户信息,因此我们可以要求用户在每次请求中都携带一个只有用户和服务器知道的 Token,并在请求中携带这个 Token 来进行验证。
为什么能要求用户请求中携带 token呢?
个人理解:对于被攻击网站,一般正常操作下,请求都是在该网站中点击后进行的,因此若是用户自己登陆后,被攻击网站的页面是由他们自己编码的,因此可以确保发送请求时都携带上一个自己服务器生成的 token,而对于外域的请求,即使携带了 Cookie,也没有这个服务器生成的 token。
对于GET请求,Token将附在请求地址之后,这样URL 就变成 http://url?csrftoken=tokenvalue
。而对于 POST 请求来说,要在 form 的最后加上:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
验证码和密码其实也可以起到CSRF Token的作用,而且更安全。
设置 Cookie 的 SameSite 属性可以让 Cookie 在跨站请求时不会被发送,从而极大程度上杜绝了 CSRF 攻击。
SameSite
有几个取值:
None
:无论是否跨站都能携带 CookieLax
:允许部分第三方请求携带 CookieStrict
:仅允许第一方请求携带 Cookie在 Chrome 80 之前 None
是默认值,之后默认是 Lax
如何判断是否是同站:需要 协议 + eTLD + 1 相同就是同站
eTLD
:effective top-level domain, 有效顶级域名,也就是在 Mozilla 维护的公共后缀列表中,如.com
、.co.uk
、.github.io
、.top
等
eTLD + 1
:有效顶级域名 + 二级域名,如yleave.top
、baidu.com
、taobao.com
等
那么按照这种规则,a.baidu.com
和b.baidu.com
是同站,而a.github.io
和b.github.io
则不是同站
请求类型 | 示例 | 正常情况(None) | Lax |
---|---|---|---|
链接 | <a href="..."></a> | 发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="..."/> | 发送 Cookie | 发送 Cookie |
GET 表单 | <form method="GET" action="..."> | 发送 Cookie | 发送 Cookie |
POST 表单 | <form method="POST" action="..."> | 发送 Cookie | 不发送 |
iframe | <iframe src="..."></iframe> | 发送 Cookie | 不发送 |
AJAX | $.get("...") | 发送 Cookie | 不发送 |
Image | <img src="..."> | 发送 Cookie | 不发送 |
美团前端技术博客
浏览器系列之 Cookie 和 SameSite 属性