什么是 CSRF
概念
跨站请求伪造(Cross-Site Request Forgery
,CSRF
)是一种网络的攻击方式。攻击者利用被攻击网站的安全漏洞,盗取用户的登录注册权限信息,诱导用户向第三方网站发送跨站请求。由于浏览器请求自动包含所有 cookie,包括会话 cookie。因此,如果用户通过了站点的身份验证,站点就无法区分合法请求和伪造请求。
攻击过程
一个典型的 CSRF 攻击主要有以下几个流程:
- 用户登录了自己信任的网站 A,并在浏览器中保存了登录凭证 Cookie
- 在登录(Session)有效的情况下,用户被诱导进入恶意网站 B,同时网站 B 向网站 A 发送了某个请求
- 网站 A 接收到请求,并根据 Cookie 验证身份成功,从而非法的操作被冒名执行
常见危害
CSRF 通常只影响 Web 应用中用户权限暴露的功能。例如,攻击者利用用户权限进行资金转移、密码更改、商品购买、邮件劫持等。
常见场景
在诱导进入攻击者网站之后,向银行系统发起转账到 Harker 账户的操作请求
<img src="http://bank.example/withdraw?account=bob&amount=1000000&for=Harker"/>
<form method="POST" action="http://bank.example/withdraw?account=bob&amount=1000000&for=Harker" enctype="multipart/form-data">
<input type="hidden" name="cf2_emc" value="true"/>
<input type="hidden" name="cf2_email" value="hacker@hakermail.com"/>
</form>
<script>
document.forms[0].submit();
</script>
CSRF 防御姿势
基于前面提到的内容,我们知道攻击形成的主要有两个要点:
- 信任了第三方站点的请求
- Cookie 驻留在浏览器并在窗口之间共享
因此,我们的应对措施也该围绕这两点展开:
- 不信任来自不明站点的请求
- 双重身份验证
Origin 安全
HTTP Referer
-
概念 HTTP 请求首部字段 Origin 包含当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
-
语法:
Referer: <url>
- 对于页面跳转,url 为当前页面被链接而至的前一页面的绝对路径或者相对路径
- 对于资源请求,url 为发起请求页面的地址
HTTP Origin
-
概念 HTTP 请求首部字段 Origin 指示了请求来自于哪个站点。该字段仅指示服务器名称,并不包含任何路径信息。该首部用于 CORS 请求或者 POST 请求。除了不包含路径信息,该字段与 Referer 首部字段相似。
-
语法:
Origin: <scheme> "://" <host> [ ":" <port> ] | "";
防御
利用 Referer/Origin 可以过滤任何来自外域/不信任站点的请求,只需在后端给所有安全敏感的请求统一配置一个拦截器来检查 Referer/Origin 的值。 但是,Referer 和 Origin 有时并不是完全安全有效的,因为它们一定程度上依赖于浏览器的具体实现。
添加 CSRF Token
完全信任 Cookie 作为验证信息是攻击成功的主要原因,因此要防御 CSRF 就要添加攻击者无法伪造的信息,并且该信息不能存储在 Cookie 中,CSRF Token 就是一种比较好的选择。
服务器生成
用户进入网站后,服务器会使用随机数、时间戳等,根据加密算法生成一个随机且唯一的 token
请求中携带
获取到 token 之后,Web 应用采取一定的机制将 Token 携带在需要安全验证的请求中,一般有两种选择的方案:
- 携带在请求地址之后:不推荐,用户体验不太好,而且浏览器的 Referer 有可能暴露信息
- 添加到 HTTP 头部信息中:目前主流的方式
服务器验证
服务器接收到用户请求之后,首先要判断 token 的有效性和合法性,校验通过才能请求成功。一般来说 token 有两种,一种是同步器 Token,另一种是加密 Token。
- 同步器 Token:token 可以在每个用户会话或者每次用户请求的时候重新生成。其中,每次请求生成加密性更强,因为它进一步减少了被盗用的可能性。当请求到达服务器,服务器根据 session ID 将 token 和存储在用户会话中的值进行比较,校验其是否存在,同时解密后校验加密字符串是否一致以及时间戳是否过期。对于分布式系统来说,采用会话维持的 token 会带来比较大的负担,通常会将值存储在 Redis 等公共存储空间中。
- 加密 Token:目前更多的应用或者系统选择加密 token 进行验证。因为这种策略无需维护额外的 session,校验合法性通常是根据 user ID 和时间戳进行的。
实践
例如,我们使用 axios
库的过程中,可以为其配置一个全局属性
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
SameSite Cookie
Cookie 的理解
- 概念 早期 Cookie 的出现是为了解决 HTTP 协议的无状态问题,它是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次再向同一服务器发起请求的时候被携带并发送到服务器上。
当服务器收到 HTTP 请求时,服务器可以在响应头里面添加一个 Set-Cookie
选项。浏览器收到响应后通常会保存下 cookie,之后在请求头部将 cookie 信息发送给服务器。
Cookie 的安全缺陷
- Cookie 是保留在本地,而且在浏览器的窗口、标签、会话中共享的
- Cookie 虽然有 Domain 属性可以支持指明哪些主机可以接受 Cookie,例如,如果设置
Domain=mozilla.org
,则 cookie 也包含在子域名中(如developer.mozilla.org
)。但是,在 CSRF 攻击中,cookie 和攻击请求是同源的,意味着这个属性无法起到安全作用。
SameSite 属性
SameSite cookies 是相对较新的一个字段,所有主流浏览器都已经得到支持。在请求响应中我们可以这样使用这个属性:
Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict
Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax
None
:浏览器会在同站请求、跨站请求下继续发送 cookieStrict
:浏览器将只在访问相同站点时发送 cookie,从而可以阻止跨站请求伪造攻击Lax
:目前新版浏览器的默认值,和Strict
属性类似,但是跨站请求可以在顶级导航和使用安全的HTTP方法下携带,比如 iframe 中点击一个链接是不会携带的。
其他
比较常用的做法是将加密的token添加到头部信息中,通过 XMLHttpRequest 这个类,一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。
其他的防御措施还有,Double Submit Cookie,结合 HTTPS 设置 Cookie 的Secure
、HttpOnly
属性等,这里就不一一阐述。