4.5 关键处理中引入的安全隐患
Web 应用中,用户登录后执行的操作中有些处理一旦完成就无法撤销,本书将此类处理称为“关键处理”27 。像用户使用信用卡支付、从用户的银行账号转账、发送邮件、更改密码或邮箱地址等都是关键处理的典型案例。
27 有些文献也把关键处理用于表示特定的副作用。
关键处理中如果存在安全隐患,就会产生名为跨站请求伪造(Cross-Site Request Forgeries,简称 CSRF)的漏洞。接下来,本节就将对 CSRF 漏洞进行详细的说明。
4.5.1 跨站请求伪造(CSRF)
概要
在执行关键处理前,需要确认该请求是否确实由用户自愿发起。如果忽略了这个确认步骤,就可能出现很大问题,比如用户只是浏览了恶意网站,浏览器就擅自执行关键处理等。
引发上述问题的安全隐患被称为跨站请求伪造(CSRF)漏洞,而针对 CSRF 漏洞进行的攻击就是 CSRF 攻击。
Web 应用存在 CSRF 漏洞时就可能会遭受如下攻击。
- 使用用户的账号购物
- 删除用户账号
- 使用用户的账号发布帖子
- 更改用户的密码或邮箱地址等
CSRF 漏洞造成的影响仅限于应用的关键处理被恶意使用,而像用户的个人信息等就无法通过 CSRF 攻击窃取 28 。
28 但是,一旦攻击者修改了用户的密码,就有可能窃取该用户的个人信息。
因此,为了预防 CSRF 漏洞,就需要在执行关键处理前确认请求确实是由用户自愿发起的。详情请参考本节的“对策”。
CSRF 漏洞总览
攻击手段与影响
首先让我们来看一下针对 CSRF 漏洞实施的两种典型的攻击模式。即“输入-执行”这种简单模式下的攻击手段以及中途包含确认页面时的攻击方法。
- “输入-执行”模式的 CSRF 攻击
此处用更改密码页面作为“输入-执行”模式下关键处理的例子。以下 PHP 脚本展示了更改密码处理的概要。
代码清单 /45/45-001.php(登录脚本)
<?php // 用来确认用户已登录的脚本 session_start(); $id = @$_GET['id']; if (! $id) $id = 'yamada'; $_SESSION['id'] = $id; ?> <body> 已登录 (id:<?php echo htmlspecialchars($id, ENT_NOQUOTES, 'UTF-8'); ?>)<br> <a href="45-002.php"> 更改密码 </a> </body>
代码清单 /45/45-002.php(密码输入页面)
<?php session_start(); // 确认登录 省略 ?> <body> <form action="45-003.php" method="POST"> 新密码 <input name="pwd" type="password"><br> <input type="submit" value=" 更改密码 "> </form> </body>
代码清单 /45/45-003.php(执行更改密码)
<?php function ex($s) { // 用于防范 XSS 的 HTML 转义及显示处理函数 echo htmlspecialchars($s, ENT_COMPAT, 'UTF-8'); } session_start(); $id = $_SESSION['id']; // 取得用户名 // 确认登录 省略 $pwd = $_POST['pwd']; // 取得密码 // 更改密码处理 将用户 $id 的密码更改为 $pwd ?> <body> <?php ex($id); ?> ○○的密码已更改为△△ </body>
这些脚本的运行示例如图 4-41 所示。
图 4-41 脚本运行示例
可见,密码在最后的 45-003.php 中被更改。然而,通过此脚本更改密码,还需要满足以下 3 个条件。
- 使用 POST 方法请求 45-003.php
- 保持登录状态
- 使用 POST 参数中的
pwd
指定新密码
而使浏览器发送满足以上条件的请求的攻击即为 CSRF 攻击。下面就是用来实施 CSRF 攻击的 HTML 文件。
代码清单 /45/45-900.html
<body onload="document.forms[0].submit()"> <form action="http://example.jp/45/45-003.php" method="POST"> <input type="hidden" name="pwd" value="cracked"> </form> </body>
这段代码为实施 CSRF 攻击的恶意网页的 HTML 源代码。攻击者将其置于互联网上,并在其中添加攻击对象网站用户可能感兴趣的内容,以引诱网站的用户前来浏览。
用户浏览此 HTML 时的情形如图 4-42 所示。
图 4-42 通过 CSRF 攻击变更密码
这种情况下,因为先前列举的变更密码所需条件都已满足,所以正规用户的密码就被成功更改为了 cracked。
图 4-43 CSRF 攻击成功
攻击者在实际发动攻击时,为了使攻击显得隐蔽,通常会采用不可见的 iframe 来布置恶意网页(45-901.html)。
图 4-44 隐藏 iframe 以进行暗中攻击
此时,根据同源策略,从 iframe 的外层(恶意网页)无法读取到内层(攻击对象)的内容,因此,CSRF 攻击虽然能够以正规用户的权限恶意使用攻击对象网站中的关键处理,却无法获取网页中显示的内容。
- 密码被更改也会导致信息泄漏
由于 CSRF 攻击者不能获取攻击对象页面,因此便无法窃取信息。但是,在使用 CSRF 攻击成功更改用户密码后,攻击者就知道了更改后的密码,从而也就能够登录应用来窃取被害人的信息了。
- CSRF 攻击与 XSS 攻击
CSRF 与(反射型)XSS 不仅名称相似,攻击流程也如出一辙,甚至连攻击的影响也有相同之处,因此将两者混淆的人不在少数。而为了区分两者,我们可以看一下图 4-45 所展示的 CSRF 和反射型 XSS 的攻击流程。根据此图可以看出,CSRF 和 XSS 在步骤①到③时大体相似,之后便产生了分歧。
图 4-45 CSRF 与反射型 XSS 的比较
CSRF 是指恶意使用服务器对步骤③中请求的处理,恶意使用的内容仅限于服务器端提供的处理。
而 XSS 的情况下,③的请求中包含的脚本则被原封不动地以响应④的形式返回,随后该恶意脚本在用户的浏览器中被执行。由于攻击者能够在用户的浏览器上执行自己准备的 HTML 或 JavaScript,因此只要是浏览器能做到的事都可以被用作攻击手段。攻击者甚至还能够通过 JavaScript 恶意使用服务器端的功能(显示在图中的话就是步骤⑤——向服务器发出恶意请求)。
由此可见,就攻击范围来说,XSS 的威胁更大,但针对 CSRF 漏洞则特别需要注意如下两点。
- CSRF 需要在设计阶段就考虑防范策略
- 开发者对 CSRF 的认知度要低于 XSS,CSRF 对策方面也没有太大进展
- 存在确认页面时的 CSRF 攻击
接下来就让我们来看一下第二种攻击模式,即输入页面与执行页面之间包含确认页面时的情况。有人觉得有了确认页面后 CSRF 攻击就行不通了,但遗憾的是这是个普遍的误解。
下面以更改邮箱地址的操作为例进行说明。一旦能够随意更改他人的邮箱地址,就可以使用重置密码等功能窃取用户密码。
确认页面将数据传递给执行页面的方法大体上有两种。一种是使用 hidden 参数(type 属性为 hidden 的 input 元素),另一种是使用会话变量。首先来看使用 hidden 参数的情况。
- 使用 hidden 参数传递参数
下图展示了更改邮箱地址操作时的页面跳转情况。输入页面中输入的邮箱地址被以 hidden 参数的形式嵌入在确认页面中,然后又被传递给了执行页面。
图 4-46 使用 hidden 参数传递参数
此模式下的 CSRF 攻击手段与没有确认页面时的情况相同。这是因为执行页面从输入(HTTP 请求)中取得邮箱地址信息这一点与之前的例子一样。所以,上面介绍的恶意 HTML 几乎是被直接用来攻击的。
- 使用会话变量传递参数
针对在确认页面和执行页面之间利用会话变量传递参数的网站,CSRF 将如何展开攻击呢?如图 4-47 所示,确认页面将接收到的邮箱地址保存至会话变量,然后再转递给执行页面。
图 4-47 使用会话变量传递参数
针对上述模式的应用程序发动攻击,需要以下两个阶段。
1. 向确认页面发送 POST 请求,使邮箱地址保存到会话变量中
2. 伺机打开执行页面
实现上述两个阶段的攻击的方法如下图所示,需要用到 2 个 iframe 元素。
图 4-48 使用 2 个 iframe 元素的两个阶段的攻击
iframe1 与恶意网页同时打开,并向确认页面发送含有邮箱地址的 POST 请求。这样一来邮箱地址就被保存到了会话变量中。
在恶意网页打开 10 秒钟后,iframe2 打开执行页面并完成 CSRF 攻击。这时,由于邮箱地址已被设置到会话变量中,因此邮箱地址就被更改为了攻击者所指定的邮箱地址。攻击成功。
有些应用采取向导的形式,要经过多个步骤才到达最后的执行页面,这种情况下,只需增加 iframe 的数量就照样能够实施攻击。
专栏:针对内部网络的 CSRF 攻击
CSRF 攻击的攻击目标并不仅限于发布到互联网上的网站。内部网络(局域网)的服务器同样也会成为攻击目标。例如,路由器或防火墙的配置页面中存在的 CSRF 漏洞就是典型案例。路由器或防火墙的管理员终端如果浏览了恶意网站,就有可能导致机器被非法设置,从而允许外部的访问入侵。
然而,实施该攻击的前提是必须要知道攻击目标中安全隐患的详细信息(URL、参数名、功能等)。而为了获取攻击所需的信息,一般可采取如下途径。
- 调查市面上贩卖的软件或仪器的安全隐患
- 退职员工等有过访问内部网络经验的人实施攻击
- 内部人员佯装外人实施攻击
由此可见,针对内部网络的 Web 系统发动 CSRF 攻击是可行的。同样,内部网络也有可能遭受 XSS 等其他被动攻击。因此,即使是内部系统,如果对安全隐患置之不理的话同样也很危险。
- 使用 hidden 参数传递参数
安全隐患的产生原因
CSRF 漏洞之所以能够产生,是因为 Web 应用存在以下特性。
(1)form 元素的 action 属性能够指定任意域名的 URL
(2)保存在 Cookie 中的会话 ID 会被自动发送给对象网站
(1)的问题在于,即便是恶意网站,也能够向攻击目标网站发送请求。而(2)的问题则在于,即便请求经过了恶意网站,会话 ID 的 Cookie 值也照样会被发送,从而导致攻击请求在认证的状态下被发送。
下图展示了常规的请求(正规用户自愿发送的请求)与 CSRF 攻击的请求(非正规用户自愿发送的请求)的区别(仅列出了主要项目)。
用户自愿发送的 HTTP 请求
POST /45/45-003.php HTTP/1.1 Referer: http://example.jp/45/45-002.php Content-Type: application/x-www-form-urlencoded Host: example.jp Cookie: PHPSESSID=isdv0mecsobejf2oalnuf0r1l2 Content-Length: 9 pwd=pass1
CSRF 攻击发送的 HTTP 请求
POST /45/45-003.php HTTP/1.1 Referer: http://trap.example.com/45/45-900.html Content-Type: application/x-www-form-urlencoded Host: example.jp Cookie: PHPSESSID=isdv0mecsobejf2oalnuf0r1l2 Content-Length: 9 pwd=pass1
比较两者后可以得知,HTTP 请求的内容几乎一模一样,只有 Referer 字段存在差异。用户自愿发送的请求中 Referer 指向密码输入页面的 URL,而 CSRF 攻击的 HTTP 请求中 Referer 却指向了恶意网页的 URL。
而 HTTP 请求中 Referer 以外的部分则全部相同。由于通常情况下,Web 应用中并不会检验 Referer 的值,所以,如果开发者没有意识去确认该请求是否由正规用户自愿发送,就无法区分两者。这时就会引入 CSRF 漏洞。
另外,虽然我们目前为止所说的都是使用 Cookie 进行会话管理的网站的情况,而事实上使用其他自动发送的参数进行会话管理的网站,同样也会受到 CSRF 攻击。具体来说,像使用 HTTP 认证、SSL 客户端认证、手机的移动 ID(i-modeID、EZ 号、终端序列号等)等进行认证的网站,都有可能受到 CSRF 攻击的影响。
对策
前面已经强调过,防御 CSRF 的关键为确认关键处理的请求确实是由正规用户自愿发送的。因此,作为 CSRF 的防范策略,需执行以下两点。
- 筛选出需要防范 CSRF 攻击的页面
- 使代码有能力辨认是否是正规用户的自愿请求
下面我们就来详细地解说以上两点。
- 筛选出需要防范 CSRF 攻击的页面
并非所有页面都需要实施 CSRF 防御策略,事实上无需防范 CSRF 的页面居多。通常情况下,Web 应用的入口并非只有一处,通过搜索引擎、社交书签、其他链接等方式都能进入到 Web 应用中的各种页面。比如 EC(电子商务)网站一般就非常欢迎通过外部链接进入到它的商品展示页面。而像这种页面就不用实施 CSRF 对策。
而另一方面,EC 网站中的购买商品、更改密码或确认个人信息等页面,就不能够任由其他网站随意执行。这样的页面就应当实施 CSRF 防范策略。
以下为 EC 网站的简易的页面跳转图。图中需要防范 CSRF 的页面为“购买”和“更改”页面 29 。需要防范 CSRF 的页面添加了阴影。
图 4-49 EC 网站的页面跳转图
鉴于上述这种情况,开发者在开发过程中,应当执行以下流程。
- 在需求分析阶段制作功能一览表,标记出需要执行 CSRF 防范策略的功能
- 在概要设计阶段制作页面跳转图,标记出需要执行 CSRF 防范策略的页面
- 在开发阶段实施 CSRF 防范策略
接下来我们就来看一下具体的开发方法。
- 确认是正规用户自愿发送的请求
确认请求由正规用户自愿发送是 CSRF 防御策略中必需的步骤。
下图中,假设将用户点击“执行”按钮后发送的请求作为用户自愿发送的请求,而非自愿的请求,即为从恶意网站发出的请求。两者的对比如下。
图 4-50 正规用户自愿发送的请求·非自愿发送的请求
具体来说,判断请求是否为正规用户自愿发送的实现方法,有如下 3 类。
- 嵌入机密信息(令牌)
- 再次输入密码
- 检验 Referer
下面就让我们来依次说明。
- 嵌入机密信息(令牌)
如果访问需防范 CSRF 的页面(登录页面、订单确认页面等)时需要提供第三方无法得知的机密信息的话,那么即使出现非正规用户自愿发送的请求,应用端也能够通过判断得知请求是否合法。用于此目的的机密信息被称为令牌(Token)。会话 ID 就是一种既简单又安全的令牌的实现方法。
下面我们就来看一下嵌入令牌并进行检验的例子。
代码清单 嵌入令牌的例子(执行页面的前一个页面)
<form action="chgpwddo.php" method="POST"> 新密码 <input name="pwd" type="password"><br> <input type="hidden" name="token" value="<?php echo htmlspecialchars(session_id(), ENT_COMPAT, 'UTF-8'); ?>"> └嵌入令牌 <input type="submit" value=" 更改密码 "> </form>
代码清单 确认令牌的例子(执行页面)
session_start(); ┌─确认令牌 if (session_id() !== $_POST['token']) { die(' 请从正规的页面进行操作 '); // 显示合适的错误消息 } // 下面将执行关键处理
通过要求提供第三方无法得知的令牌,从而成功防御了 CSRF 攻击。
在页面跳转有三次以上的情况下,如“输入-确认-执行”模式,嵌入令牌的页面也同样应当为执行页面的前一个页面。
另外,接收令牌的请求(接收关键处理的请求)必须为 POST 方法。因为假如使用 GET 方法发送机密信息的话,令牌信息就有可能通过 Referer 泄漏出去 30 。
专栏:令牌与一次性令牌
有一种令牌叫作一次性令牌。一次性令牌使用一次后即作废。因此每当需要一次性令牌时都会生成不同的值。生成一次性令牌时通常使用密码学级别的伪随机数生成器(参考 4.6.2 节)。
一次性令牌经常被用于需要防范重放攻击(Replay Attack)的情况下。重放攻击是指,在监听得到加密的请求后,将该请求原封不动地再次发送而达到伪装的效果。一次性令牌能有效防御重放攻击。
关于一次性令牌是否应该用于 CSRF 的防范策略,目前为止还没有形成统一的认识。虽然有人主张使用一次性令牌会提升安全性,但基于以下理由,本书并不推荐使用一次性令牌。
- CSRF 攻击与重放攻击毫不相干,因此并非一定要使用一次性令牌
- 没有证据能说明一次性令牌比使用会话 ID 作为令牌的方法更安全
- 使用一次性令牌有时会导致正常的操作也出错
另外,在一些介绍一次性令牌的书籍中,很多生成令牌的方法并不安全。例如,使用不安全的随机数,或者使用当前时间的方法等。这些方法都不如使用会话 ID 作为令牌值安全。
因此,应当避免自己生成一次性令牌的方法。
- 再次输入密码
让用户再次输入密码,也是用来确认请求是否由用户自愿发起的一种方法。
除了用来防范 CSRF 攻击,再次输入密码也可以被用于其他目的。
- 在用户下订单之前,再次向用户确认购买意向
- 能够确认此时在电脑前操作的确实是用户本人
因此,当页面有上述需求时,最好采用再次输入密码的方法来防范 CSRF。而对其他的页面(如注销处理)来说,让用户再次输入密码,反而会降低应用的易用性 31 。
前面在讲解 CSRF 攻击时所列举的密码变更功能是安全性方面的重要功能,因此,为了再次确认操作者确实为用户本人,要求用户再次输入密码是目前非常普遍的一种方式 {32[不仅需要输入当前的密码,由于密码的输入框通常看不到输入值,为了防止输入错误,新密码的情况下一般会要求输入两遍。]}。 不论是有 3 个以上页面的“输入-确认-执行”模式,还是向导模式,要求确认密码的页面都应该是最后的执行页面。如果仅在中途的某个页面进行密码确认,根据代码实现方法还是可能会存在 CSRF 漏洞,所以要求输入密码的时机非常重要。
- 检验 Referer
在执行关键处理的页面确认 Referer,也是 CSRF 的一种防范策略。正如“安全隐患的产生原因”这一小节所讲述的那样,正规请求与 CSRF 攻击请求的 Referer 字段的内容不同。正规请求中 Referer 的值应该为执行页面的上一个页面(输入页面或确认页面等)的 URL,这一点一定要得到确认。下面就是检验 Referer 的示例。
if (preg_match('#\Ahttp://example\.jp/45/45-002\.php#', @$_SERVER['HTTP_REFERER']) !== 1) { die(' 请从正规的页面进行操作 '); // 显示合适的错误消息 }
检验 Referer 的方法也存在缺陷。因为如果用户设置为不发送 Referer,页面就会无法正常显示。通过个人防火墙或浏览器的插件等禁止 Referer 的用户不在少数。另外,手机的浏览器中也有不发送 Referer 的浏览器和能够关闭发送 Referer 功能的浏览器。
另外,检验 Referer 时还容易产生疏漏,这一点一定要引起注意。例如,下面的检验就存在安全隐患。
// Referer 检验存在漏洞的示例 if (preg_match('#^http://example\.jp#', @$_SERVER['HTTP_REFERER']) !== 1) { // 以下为错误处理 // 能够绕过上述校验的示例 URL(域名为 example.com,而非 example.jp) // http://example.jp.trap.example.com/trap.html
问题出在 example.jp 后面的 / 没有得到检验。检验 Referer 时,必须要使用前方一致检索检验绝对 URL,包括域名后的 /。
另一方面,检验 Referer 方法所需的代码量是最少的。因为其他两种方法都需要在 2 个页面中追加处理,而检验 Referer 方法只需要在执行关键处理的页面上追加处理即可。
综上所述,通过检验 Referer 来防范 CSRF 漏洞的方法,其适用范围应该被限定在对公司的内部系统等能够限定用户环境的既有应用实施安全隐患对策的情况。
- CSRF 防范策略的比较
这里,我们对以上讲述的三种 CSRF 防范策略加以比较归纳,如表 4-12 所示。
表 4-12 CSRF 防范策略的比较
嵌入令牌 再次输入密码 确认 Referer 开发耗时 中 中 *1 小 对用户的影响 无 增加了输入密码的麻烦 关闭了 Referer 的用户无法 正常使用 能否用于手机网站 可 可 不可 建议使用的地方 最基本的防御策略,所有情况下均可使用 需要防范他人伪装或者确认需求很强的页面 用于能够限定用户环境的既有应用的 CSRF 防范策略 *1 如果作为既有系统的 CSRF 防范策略而从后期添加的话,因为需要修改页面,所以可能会非常耗时。
- CSRF 的辅助性对策
执行完关键处理后,建议向用户注册的邮箱发送有关处理内容的通知邮件。
发送通知邮件虽然不能防范 CSRF 攻击,但是在万一遭受了 CSRF 攻击的情况下能在第一时间让用户知情,从而将损害降到最低。
另外,除了 CSRF 攻击之外,在攻击者通过 XSS 攻击伪装成用户操作关键处理时,发送通知邮件也能够使用户尽早发现,可谓大有裨益。
但是,由于邮件是未经加密的明文传输,因此,最好不要在邮件中添加重要信息,而只是通知用户有人恶意执行了关键处理。而如果用户想要了解详情的话,可以登录 Web 应用查看购买历史或发送历史等内容。
- 对策总结
CSRF 漏洞的根本性防范策略如下。
- 筛选出需要防范 CSRF 的页面
- 确认是正规用户自愿发起的请求
其中,确认请求确实由用户自愿发起的方法有以下三种。三种方法的比较请参考表 4-12。
- 嵌入机密信息(令牌)
- 再次输入密码
- 检验 Referer
另外,作为 CSRF 漏洞的辅助性对策,可以执行以下操作。
- 执行完关键处理后,向用户注册的邮箱发送通知邮件
29 “添加到购物车”页面也需要防范 CSRF。不过,即使被第三方随意添加了购入商品,用户在付款前也应该能够察觉到。因此,如果作为一种营销模式而允许外界添加商品的话,就可以选择不对该页面执行 CSRF 防范策略。
30 HTTP/1.1 的规格文档 RFC2616 中记载了含有更新处理的页面不应使用 GET 方法(9.1.1 节),由此可见,需要防范 CSRF 的页面本来就不应该使用 GET,而应当使用 POST 方法。
31 注销处理对安全性的影响度较低,所以很多情况下会容许存在隐患。而且,就算针对注销处理采取 CSRF 防范策略,注销前让用户再次输入密码也会让人感觉极不自然。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论