概述
CSRF(Cross-Site Request Forgery)
,即跨站请求伪造,是一种网络攻击方式。在这种攻击中,恶意用户诱导受害者在不知情的情况下执行某些操作,通常是利用受害者已经登录的身份,向受害者信任的网站发出恶意请求。
原理分析
从概述中划重点:利用受害者已经登陆的身份(如登陆态、浏览器中的Cookie
等),向受害者已登陆的网站发出恶意请求。
举例:
假设用户 A 已经登陆到某银行网站bank.com
,并且恶意用户 B 创建了一个恶意网站evil.com
。当用户 A 访问evil.com
时,该网站发起了一个请求,类似于以下的内容:
<!-- 假设 bank.com 的 /transfer 接口是转账接口,amout 是转账金额,to_account 是转账对象的账户 -->
<img src="http://bank.com/transfer?amount=1000&to_account=B" />
那么因为用户 A 已经登陆了bank.com
,那么在发起这个请求的时候,浏览器就会自动携带用户 A 的登陆Cookie
去调用这个接口;从bank.com
的服务端来看,这个请求已经携带了用户 A 的Cookie
,是一个合法请求,那么就会正常执行这个(转账)请求;最终(1000
块)钱就从用户 A 转到了用户 B 的账户。这就是典型的CSRF
攻击。
上面的例子比较夸张(实际上银行系统的防御能力堪称全方位无死角,所以大家也不用担心这种问题会实际发生),但是CSRF
攻击的原理就是这样。
问题的关键
从上面的例子来看,问题出现的点在于:
- 当用户使用浏览器在
evil.com
的网页上请求bank.com
的接口时,浏览器自动带上了bank.com
的Cookie
。 bank.com
没有校验请求来源是否是受信任的网页。
所以对应的,如果可以解决这两个问题,那么CSRF
问题自然就解决了。
解决问题的办法
分别从浏览器端和服务器端来解析一些解决CSRF
问题的机制。
浏览器端
SameSite Cookie 属性
在服务器返回Cookie
给浏览器时,可以在Set-Cookie
响应头中添加SameSite
属性,后续浏览器将会按照SameSite
的值来决定什么情况下允许跨站请求携带该Cookie
,例如:
SameSite
属性有三种取值:
SameSite=Strict
:浏览器仅在同源请求中携带该Cookie
,跨站请求(如从evil.com
发起的对bank.com
的请求)不会附带该Cookie
。SameSite=Lax
:允许某些跨站请求(如GET
请求)携带Cookie
,但不会在POST
等修改数据的请求中携带。SameSite=None
:允许所有跨站请求携带Cookie
(这也需要设置Secure
属性,确保只通过HTTPS
传输)。
再代入到之前讲过的例子中,在evil.com
网页中请求bank.com
的接口时,如果设置了SameSite=Strict
,那么浏览器在执行该请求时将不会自动带上bank.com
的Cookie
,这样在一定程度上就能防止CSRF
攻击。
响应头 Referrer Policy 和请求头 Referer 属性
在讲Referrer Policy
之前,先讲一下请求头中的Referer
属性。
从Referer
的字面意思是引用者的意思,在请求头中,它描述了当前请求是从哪个具体页面跳转过来的。在浏览器环境下,Referer
属性是在浏览器发起请求时由浏览器自动设置的,通常情况下网页中的脚本是无法手动指定或更改的。
例如,你在银行网站的bank.com/user/123
(假设123
是你在银行系统内的用户ID
),那么从这个界面跳转到evil.com
的界面时(只是举例说明,尽管从银行系统跳转到一个恶意网站基本是不可能的事情),默认情况下这个跳转请求的请求头上将会添加Referer=bank.com/user/123
,用来告诉evil.com
请求是从bank.com/user/123
界面跳转过来的。
在这个例子中,evil.com
很有可能从Referer=bank.com/user/123
中拿到你在银行系统的用户ID
是123
,你的用户信息就被泄露了,你账户中的钱就多了一分被窃取的可能。
Referrer Policy
是由服务端通过HTTP
响应头或HTML <meta>
标签设置的,用于控制浏览器在发起请求时是否以及如何发送Referer
头。它可以限制浏览器在跨站请求中暴露的来源信息,可以保护用户隐私。
服务器可以在请求响应头中设置Referrer Policy
的值,例如:
Referrer-Policy: no-referrer
或者通过网页中的<meta>
标签设置,例如:
<meta name="referrer" content="no-referrer">
根据 Referrer-Policy
的值,浏览器会在后续请求中控制 Referer
请求头的发送。Referrer-Policy
属性有四种取值:
no-referrer
:不发送Referer
头。no-referrer-when-downgrade
:默认值,在HTTPS
页面发起HTTP
请求时不发送Referer
,其他情况下发送完整的Referer
。origin
:只发送来源站点的根URL
,不包含具体路径。strict-origin-when-cross-origin
:同源请求发送完整的Referer
,跨站请求只发送源站点的根URL
。
在上面讲的例子中,如果我们设置了Referrer-Policy: no-referrer
,那么从bank.com
跳转到evil.com
的时候,就不会在请求头中添加关于你用户信息相关的内容,你的信息就不会泄漏,这样也能减少你被CSRF
攻击的可能。
Access-Control-Allow-* 系列响应头属性
Access-Control-Allow-*
是一系列由服务端返回的响应头属性,主要包括:
Access-Control-Allow-Origin
:指定哪些域名(来源)被允许访问其资源。Access-Control-Allow-Methods
:指定允许的HTTP
方法,如GET
、POST
、PUT
等。Access-Control-Allow-Header
:指定允许的自定义请求头,如Content-Type
、Authorization
等。Access-Control-Allow-Credentials
:指定是否允许发送Cookie
和其他凭据。如果设置为true
,则表示允许。Access-Control-Expose-Headers
:指定哪些响应头可以被浏览器访问(即哪些头是可以暴露给前端JavaScript
代码的)。
与跨域访问最相关的就是Access-Control-Allow-Origin
。例如,bank.com
的服务器可以在响应头中添加Access-Control-Allow-Origin=bank.com
来限制只有来自于这个域名的请求才允许发起访问,这种情况下,evil.com
自然也没有办法发起对于bank.com
的请求了。其他几个响应头都可以针对浏览器发起的请求行为进行不同维度的限制,在一定程度上也可以防止CSRF
攻击。
服务器端
检查请求头 Origin 属性
从Origin
的字面意思是起源的意思,在请求头中,它描述的是请求发起的源站点,包括协议、域名、和端口,但不包括具体的路径。
与Referer
相比,Origin
更关注大范围的来源,即请求是从哪个站点发起的(比如是从 https://www.example.com
还是从 http://another.com
发起的),而不在意具体的页面路径。Origin
请求头通常用于跨站请求或安全相关的场景。比如,当你发送跨域请求时,浏览器会自动附加 Origin
头,告诉目标服务器请求的发起源,以便服务器决定是否允许请求。
与Referer
属性类似,在浏览器环境下,Origin
属性是在浏览器发起请求时由浏览器自动设置的,通常情况下网页中的脚本是无法手动指定或更改的。
例如,当你在evil.com
网页被诱导向bank.com
发送请求时,请求头上将会自动添加Origin=evil.com
,银行的服务器接收到请求后,通过查看Origin
属性,就可以知道这个请求发起自evil.com
而不是bank.com
。
所以在服务器接收到请求后,可以检查请求头中的Origin
属性,判断请求是否来源于受信任的网页,如果不是受信任的网页,就可以丢弃该请求不作处理。
使用验证码
经过上面的分析,我们已经知道了CSRF
攻击有一个重要的前提就是在用户不知情
的情况下自动请求目标系统,所以如果我们能在重要的接口操作前,通过验证码验证用户真实身份,确保用户知晓接下来的操作行为,也可以防止CSRF
攻击。
使用 CSRF Token
在用户访问并登陆一个网站(例如bank.com
)之后,服务器会生成一个唯一的CSRF Token
,并将其发送给客户端。后续使用该用户的身份信息进行访问的接口都需要客户端在请求中添加该Token
,服务端也会校验该Token
,用来确保用户的真实身份。
注意,这个Token
在浏览器中的的存放位置,与服务端交互时的使用规则(该Token
是放在请求头还是请求体中,参数的具体名称,是否需要根据当前时间戳再次生成)每个网站都可能是不同的,所以使用CSRF Token
可以在较大程度上防止CSRF
攻击。