由于 HTTP 协议是无状态的,需要浏览器协助保存会话信息。当用户在网站上登录后,服务器将认证信息通过 Cookie 等手段保存在浏览器中。随后发起 HTTP 请求时,浏览器将自动携带 Cookie 数据,以此保持认证状态。
用户在网站上的一切操作,比如发邮件、发消息,甚至是银行转账,都是通过 HTTP 请求进行的。只要用户的登录认证信息尚未过期,浏览器就会自动将其带上,从而让 HTTP 请求通过认证。
利用这一点,黑客可以诱导用户在一个已认证过的浏览器上执行非本意操作,并从中非法牟利。这就是臭名昭著的 跨站请求伪造( cross-site request forgery ),简称 CSRF 。
CSRF 利用网站对用户浏览器的信任,诱导用户访问一个自己登录过的网站,并发出非本意的请求。因为像 Cookie 这样的简单身份验证机制,只能保证请求是由认证过的浏览器发出的,而不能保证请求一定出于用户本意。
攻击原理
HTTP 是一种无状态的应用协议,请求和请求之间在协议层面是互相独立的。因此,Web 网站通常在用户登录后,将认证信息保存在浏览器 Cookie 中,以便后续的请求能够顺利通过认证。以某银行网银 somebank.com 为例:
- 用户在登录表单输入账号密码,点击按钮发起登录请求;
- 服务器检查账号密码,验证通过则设置一个 Cookie ,保存认证信息;
- 浏览器后续发起请求时,将自动携带 Cookie ,从而保持住登录状态;
- Cookie 失效后,HTTP 请求因失去登录状态而失败;
如果黑客利用这个特性,诱导用户发起其精心捏造的 HTTP 请求,后果将不堪设想。
举个例子,假设某网银的转账接口为 GET 请求,参数如下:
|
|
如果黑客想让别人给自己转 1000 块钱,只要想办法让他登录网银后,请求这个 URL 即可:
http://somebank.com/api/withdraw?amount=1000&for=hacker
换句话讲,任何登录过网银(尚未失效)的人,只要访问这个 URL 一次,就给黑客转 1000 块钱!因此,只要黑客能够诱导用户访问这个 URL ,就能源源不断地收黑钱!
诱导用户访问 URL 并非难事,将其做成一个劲爆链接即可:
|
|
只要标题足够有吸引力,用户就会点击。但他不知道的是,链接地址其实是网银转账接口的地址!如果他用浏览器登录过该网银(且尚未失效),一点击连接就发起转账请求,给黑客转 1000 块钱!
您可能会说,GET 请求才能通过链接诱导用户发起,改成 POST 请求就安全了。GET 请求限制确实比 POST 少,从这个角度看 POST 是比 GET 要安全一些。但 POST 请求也不是绝对安全的,它虽然无法通过链接简单触发,但可以借助表单来触发。
|
|
攻击实验
笔者提供了一个带有 CSRF 漏洞的 Web 应用 IdeaHub ,可以用来练习 CSRF 攻击。如果您看过 SQL注入 或 XSS 相关章节,对它应该不陌生。CSRF 漏洞位于应用的打赏接口 POST /reward
,接口处理逻辑如下:
|
|
在用户登录尚未失效的前提下,接口从 POST 上来的表单中取出相关参数后,便执行金币转账操作。因此,黑客可以制作一个隐藏表单,再诱导用户点击表单提交按钮。
|
|
请看攻击代码示例,这是一个 form 表单,采用 POST 方法提交给 http://localhost:5000/reward?receiver_id=2 ,即 IdeaHub 的打赏接口。其中,金币接收人通过 receiver_id 指定为黑客的 ID ,而打赏数额则通过隐藏字段 coins 来提交。
注意到,表单提交按钮被 CSS 样式伪装成一个普通链接。只要配上一个劲爆的标题和图片,浏览这个网页的用户就很有可能会点开看看。如果他登录过 IdeaHub 而且尚未失效,接口请求就会成功。这相当于向黑客打赏金币,而且自己完全不知情!
实验环境
我将这个应用打包成一个镜像,只要安装了 Docker 环境,即可用这个命令一键启动:
|
|
实验环境启动后,可以在屏幕中看到两个窗口:左边窗口跑着 Web 服务,可以看到它的日志输出;右边窗口是一个 shell 命令行,可以用来执行一些命令。
如何在不同窗口间进行切换呢? 我们来复习一下 tmux 窗口切换操作:
- 按下 tmux 功能键
Ctrl-B
;- 再按下
Q
,这时每个窗口会出现一个数字;- 按下窗口上的数字,即可切到对应的窗口;
这个 docker 命令将容器的服务端口 5000 映射到宿主机,因此只需在浏览器中输入 URL http://localhost:5000 ,即可访问部署好的应用,来到登录页。
已知系统中有 alice
、bill
、 eric
、lucy
等用户,可以随便选一个来登录。登进去后,可以到金币排行榜 http://localhost:5000/rank 查看自己的金币数量。笔者扮演黑客角色,请顺便留意下我的账号 fasion 下的金币。
用户 | 密码 |
---|---|
alice | alice123 |
bill | bill123 |
eric | eric123 |
lucy | lucy123 |
笔者将攻击网页部署在 https://cors.fasionchan.com/network-lab/csrf-hacking.html ,打开可以看到一个图片和一个如何年入百万的链接。很想点开看看是不是?那就点吧!点之前请确保已经登录过 IdeaHub !
攻击网页内部其实是一个表单,点击链接就将表单提交到 IdeaHub ,向 fasion 打赏 10 个金币!刷新金币排行榜,看看自己的金币是不是少了 10 个,而 fasion 的金币是不是多了 10 个!
这也太可怕了吧!我以为在另一个网站 cors.fasionchan.com
上操作,结果被诱导到 IdeaHub 上做转账,而且毫不知情!这就是 CSRF 攻击的典型案例,核心在于利用 IdeaHub 对当前浏览器的信任(登录未失效)。
一些版本较新的现代浏览器,从一个域名请求另一个域名时,不允许携带 Cookie 数据。比如 Chrome 内置了一个新特性 SameSiteByDefaultCookies ,仅允许来自同一站点的请求携带 Cookie 。
因此,在
cors.fasionchan.com
将表单提交给localhost:5000
时,Chrome 不会携带 Cookie ,也就无法完成攻击实验了。不过我们可以换一个安全策略宽松一点的浏览器来实验,比如 Firefox 。
防御手段
CSRF 漏洞危害很大,该如何防御呢?
您可能会有疑问,刚刚不是提到:现代浏览器已经有一些安全保护了吗?还需要特地防御吗?我们不知道用户到底使用什么浏览器,很有可能是非常老旧的浏览器。因此,我们不能将安全都寄托在浏览器身上。
想要防御 CSRF 攻击,后端除了验证浏览器登录信息外,还需要做额外的检验。
- Referer 检验;
- 同步令牌检验;
- 验证码校验;
Referer
按照 HTTP 协议规范,浏览器发起请求会通过 Referer 头部,将来源页面地址告诉服务器。因此,服务端只需检查 Referer 中的域名,将从外部网站过来的请求拦截即可。
这个方法非常简单,实现成本也很低,但可靠性不高。因为 Referer 头部是浏览器负责提交的,并不是所有浏览器都会按照规范做。如果浏览器有漏洞,同样会被利用。因此,保险起见,不能将生命线交给浏览器。
同步令牌检验
当用户请求网页时,后端随机生成一个令牌作为隐藏字段嵌在 HTML 表单中。当用户提交表单时,令牌也会作为其中一个字段发送到服务器,服务器检查令牌跟自己生成的是否匹配。这样确保黑客进行 CSRF 攻击时,由于令牌缺失而无法通过校验。
现在,我们尝试对 IdeaHub 加以改造,修复 CSRF 漏洞。我们将用户 ID 进行签名后作为令牌嵌入表单隐藏字段,收到表单提交后对令牌进行签名验证,再对比用户 ID 是否匹配。
先补充一点数据签名的基础知识,以 itsdangerous.TimestampSigner 为例,首先初始化一个签名器:
|
|
参数为签名秘钥,这是防伪的关键,必须妥善保管,不可泄露。
有了签名器后,我们就可以对数据进行签名:
|
|
结果包含 原数据 、时间戳 和 签名 ,中间以英文句点分隔。可以调用 unsign 方法对结果进行验证,验证通过则返回原数据:
|
|
数据签名可以有效防御数据伪造和篡改,我们便利用这个特性对用户 ID 进行签名后作为 CSRF 防御令牌:
|
|
- 函数 current_user_id_in_bytes 获取当前用户 ID ,并编码成 bytes 字节序列对象;
- 函数 new_csrf_token 对当前用户 ID 进行签名,作为 CSRF 防御令牌;
- 函数 validate_csrf_token 验证 CSRF 防御令牌,它先对令牌签名进行验证,再对比原数据是否为当前用户 ID ;
- 函数 utility_processor 将 new_csrf_token 注册到模板引擎上下文,这样我们就可以在模板中生成令牌;
接下来,我们在金币打赏表单中添加隐藏字段 csrf_token ,调用 new_csrf_token 生成一个令牌作为字段值:
|
|
模板引擎渲染完毕后,表单中的隐藏字段大致如下:
|
|
- 字段名为 csrf_token ;
- 字段值为 CSRF 防御令牌;
最后,我们在打赏表单处理函数中,对 csrf_token 字段中的令牌进行校验,验证不通过则直接报错:
|
|
这样一来,CSRF 漏洞就彻底堵住了。因为没有签名秘钥,黑客无法伪造令牌!
强烈建议大家将 IdeaHub 跑起来,并亲自修复漏洞,进一步加深理解。new_csrf_token 等四个函数已经写好,您只需在 templates/reward.html 模板中的表单添加令牌字段,并在 ideahub.py 中的 reward 函数进行校验即可。
如果您不知如何在命令行下编辑文件,可以试试 nano 命令,简单易用:
- 执行 nano 加文件路径打开文件,例如:
templates/reward.html
;- 用方向键移动光标;
- 按
Ctrl-O
保存,nano 提示待写入文件名,按回车确认;- 按
Ctrl-X
退出编辑,其他操作请看底部提示;
验证码校验
重要接口可以加入短信验证码校验,验证码错误就拦截,这样就彻底杜绝 CSRF 的可能性。
总结
CSRF 漏洞利用网站对用户浏览器的信任,诱导用户访问一个自己登录过的网站,并发出非本意请求,危害很大。为防御 CSRF 攻击,我们可以采用同步令牌校验法,方法很简单:
- 生成不可伪造的令牌,并嵌在页面中(例如 form 表单隐藏字段、meta 标签或 JS 代码等);
- 客户端请求接口时,同时将令牌送到服务器;
- 服务器检查令牌是否正确,并据此拦截恶意请求;
【小菜学网络】系列文章首发于公众号【小菜学编程】,敬请关注: