别人的数据,你的安全:企业聚合应用,第 2 部分





5.00/5 (1投票)
在本系列的第一篇文章中,我介绍了跨域资源共享(CORS)和构建 iframe 沙箱,并描述了如何在聚合应用中使用这些技术来消费来自其他域的数据,并为深度防御策略提供一个保护层。
在本系列的第一篇文章中,我介绍了跨源资源共享 (CORS) 和构建 iframe 沙箱,并描述了如何在混搭应用程序中使用这些技术来使用来自其他域的数据,并为深度防御策略提供一个层次。在本文中,我将开始探讨如何通过定义信任级别然后进行相应的清理来使用来自 CORS 连接(或任何 Ajax 连接)的数据。为此,我将基于 Project Silk 提供的指导进行构建。首先,让我们讨论信任以及企业中的混搭应用程序如何对现有范式提出独特的挑战。
信任或缺乏信任
在《编写安全代码》一书中,作者提出了一个极好的口号:“所有输入都是邪恶的。”在企业聚合应用的世界里,这是事实,但有些输入比其他输入更邪恶。例如,来自公司人力资源系统的数据源与来自 Twitter 的数据源构成的威胁相同吗?另一个关于软件安全的常见说法是“所有外部系统都构成威胁”。同样,这也是事实,但安全关乎风险管理,这需要在您的聚合开发过程中保持核心焦点。风险是威胁(威胁是对系统漏洞的利用)的影响及其执行概率的组合。HR 数据源呈现的威胁类型可能比使用 Twitter 源可能实现的威胁影响要小。Twitter 源可以是任何用户提供的任何文本内容,而 HR 源则是由 HR 部门成员提供和验证的一组结构化数据点。这使得 HR 系统的风险较低(假设执行概率低于或等于 Twitter)。在考虑系统的信任时,请将您的输入视野扩展到包括来自您的聚合提供商的内容,并权衡内部与外部提供商的信任。
在为聚合提供商构建输入验证时,需要考虑以下一些问题
- 您的组织对风险的文化是什么?您是风险规避型,还是在功能受到威胁时不太关心风险?
- 提供商的历史如何?例如,如果您知道一个内部提供商曾被心怀不满的员工在其网站上发布恶意代码,您显然需要密切关注这个提供商。
- 您从提供商那里收到什么样的数据?以 Twitter 和 Bing 地图为例。来自 Bing 地图的内容类型与来自 Twitter 的内容类型不同(取决于 API)。这对您对提供商的信任有何影响?
- 数据是否跨越了您的威胁模型中的信任边界?
聚合数据 = 输入
总的来说,您的目标是安全地使用来自各种来源的数据,其中一些是可信的,而另一些则不那么可信。“来自各种来源的数据”是“输入”的一种冗长说法,这意味着您需要考虑输入验证的三个要素:约束、拒绝和清理。约束输入不仅限于允许的内容,还意味着减少系统可能的入口点。(在信息安全中,我们称之为“减少攻击面。”)您需要一个输入瓶颈,所有使用的数据都必须通过它。幸运的是,Microsoft 模式与实践的优秀人士通过 Project Silk 中的数据管理器提供了一个很好的设计来做到这一点。
为数据管理器提供的示例代码是处理我们将在示例应用程序中使用的 Ajax 请求的绝佳单点。作为处理请求的组件,数据管理器非常适合容纳我们输入验证过程的组件。图 1 显示了在我们的应用程序中使用增强型数据管理器时,Ajax 请求遵循的工作流程。
我们有两个验证活动
- 验证响应:此过程对来自 Ajax 请求的内容执行输入验证活动。此时,我们已从提供商处收到数据,需要确保其符合我们应用程序配置的安全标准。
- 验证缓存:此过程对来自缓存的内容执行输入验证活动。切勿假设缓存中的数据是干净的。尽管我们的应用程序会在将数据存储到缓存之前对其进行清理,但我们需要确保从缓存中提取数据时,数据仍然是干净的。操纵开发者认为是干净的缓存有时被称为缓存投毒。这种类型的攻击利用开发者对缓存的错误信任,通过向缓存中注入恶意内容,从而颠覆任何针对来自缓存的数据的输入验证活动。
我们将使用一个单独的构件来管理这两个验证活动。利用 Project Silk 的设计,我们将构建一个验证构件,它执行输入验证的三个过程:约束、拒绝和清理。在 《改进 Web 应用程序安全性:威胁与对策》(2003 年)中可以找到关于输入验证技术的优秀、简洁的资源。尽管微软已停用此内容,但这本书是 Web 应用程序安全的绝佳资源,即使您不是 ASP.NET 开发者,我也强烈推荐它。
该过程的第一步是将数据限制为已知的好数据。当您看到“已知的好数据”时,立即想到白名单。在白名单方法的输入验证中,您确定允许的内容并拒绝所有其他内容。以下是您可以用来执行此操作的一些示例技术
- 类型检查: 请求返回的数据是否与信任代理所说的返回数据匹配?在 JavaScript 中实现这样的检查可能是一个挑战,因为“类型”的概念并不总是清晰明确的。幸运的是,您可以限制应用程序将接受的数据类型,并只接受通过这些数据类型检查的内容。
- 长度检查:使用正则表达式检查返回的长度是否符合您的预期。例如,Twitter feed 的长度是 140 个字符,是吗?小心假设。Twitter feed 在视觉上是 140 个字符长,但可能包含支持性 HTML(例如缩短的 URL),您也需要考虑到这一点。
- 格式检查:在返回的数据中搜索您期望的模式。例如,如果您期望返回一个由三个逗号分隔的项目组成的列表,请使用正则表达式检查数据是否符合此格式。不符合即表示数据无效。
- 范围检查:您期望一个介于 1 和 10 之间的数字吗?范围检查可以将您的数据限制在一个适当的具体范围内。例如,在人力资源系统中,一个人的年龄值为 259 可能不是有效的。
对于我们的小部件,我们将专注于类型检查。其他元素是对小部件的极好扩展,可以在特定情况下使用。例如,当数据由 UI 小部件渲染到页面时,您可以添加一个长度验证器。我将描述的是一个输入验证小部件的骨架和基本框架,我鼓励您用本文讨论的主题对其进行扩展。
限制输入
验证小部件使用与 Project Silk 的数据管理器相同的设置。这与标准的 UI 小部件略有不同,但此控件不创建界面,它只清理数据。与许多小部件一样,我们首先设置控件的可能选项。这些是默认设置,在控件实例化时将被替换
(function (hybrid, $) { hybrid.sanitizer = { //default options options: { data: null, //data to be validated dataType: null //[OPTIONAL] expected data type of the data },
有许多默认的构件方法,例如 Destroy
、_create
和 _init
,但我们在这里不会实现它们。作为我们主力的方法是 sanitize
方法。此方法从任何地方获取输入,将其约束到我们接受的类型,并对数据进行编码以使其成为更安全的输出。
//validate data provided sanitize: function (inputOptions) { var that = this; that.options = $.extend({}, this.options, inputOptions); var suppliedDataType = null; var output = null;
首先,和 Project Silk 代码一样,我们将 `this` 转换为 `that` 以避免在 `this` 变成其他东西的嵌套函数中产生混淆。接下来,我们使用 jQuery 的 `extend` 方法创建一个新的 `options` 对象,该对象将默认值与调用 `sanitize` 方法时提供的 `inputOptions` 的值相结合。这一步确保了我们的 `options` 对象中提供了所有值。两个将保存重要信息的变量是 `suppliedDataType`(通过识别所提供数据的数据类型来确定)和 `output`(代表方法结束时返回的内容)。
虽然我们在`options`对象中有一个`dataType`设置,但我们希望依赖于我们能从数据中确定的内容。这是约束数据的一个方面。我们只接受特定类型的数据。您接受什么数据取决于您自己,但就本项目而言,我们只需要 JSON 和 XML。话虽如此,让我们看看关于这两种数据类型的一些具体问题。
JSON
JavaScript 对象表示法 (JSON) 是一种非常常见的混搭数据格式。如果您使用 jQuery 进行 Ajax 调用并指定 json 作为数据类型,jQuery 将为您进行转换,以得到一个有效的 JSON 对象(如果结果不是有效的 JSON,则会抛出异常)。如果您不使用 jQuery,那么根据您将文本转换为 JSON 对象的方式,您可能会遇到问题。很多时候,当 JavaScript 开发人员需要将文本转换为对象时,他们会想到使用 `eval()`,但有更好的方法。这里有一些使用 jQuery 将文本转换为 JSON 的示例代码。您可以使用 JSON.parse 遵循相同的过程,它内置于较新版本的 Internet Explorer 和大多数其他浏览器中。对于缺少此功能的旧版浏览器,您需要包含 Douglas Crockford 的 JSON2.js 来实现该功能。
try { output = $.parseJSON(that.options.data); if (null !== output) suppliedDataType = "json"; } catch (ex) { }
在这个代码块中,jQuery `parseJSON` 方法检查传入 `data` 对象的数据是否是有效的 JSON 对象。如果你使用 `dataType : "json"` 进行 Ajax 调用,你肯定会得到一个 JSON 对象。这个检查是为了当数据类型是文本时,或者当使用多种数据类型并且由于某种原因只返回了文本时。如果 `output` 有一个值(如果数据被解析为 JSON,它应该有),你将 `suppliedDataType` 变量设置为 `json`。以防数据不是有效的 JSON,代码将该部分包装在 `try/catch` 中,因为 jQuery 的 `parseJSON` 方法如果无法将数据转换为 JSON,将会抛出一个异常(不像我们稍后会看到的 `parseXML`)。
eval() 还是不 eval()
在安全领域,`eval()` 的使用是一个棘手的话题。当你向安全专业人士描述它的功能时,通常会得到一种惊慌失措的反应。基本上,`eval` 接收数据并将其转换为命令,这意味着数据和命令通道发生了可怕的交叉,从而导致像 XSS 和注入这样的攻击。然而,对于开发人员来说,有时 `eval` 是解决代码问题的答案。作为一个开发人员(或开发团队),你需要在功能和安全之间取得平衡。安全最佳实践是永远不要对不受信任的数据使用 `eval`(这就回到了之前对什么是受信任的审视)。不幸的是,代码问题并不总是符合最佳实践。当你需要使用 `eval` 时,要意识到使用它会给你的应用程序带来风险。确保在你的威胁模型中记录 `eval` 的使用,因为安全部门会希望了解这一点。
JSONP
如果您想从应用程序以外的来源获取 JSON 对象(还记得第一篇文章中的同源策略吗?),JSONP 可能是您的答案。JSONP 的工作原理是在发出 Ajax 请求的页面上插入一个脚本标签,该标签调用远程站点的 JSON 对象。远程站点接收请求,并将 JSON 对象包装在一个由“callback”查询字符串参数定义的方法中。然后,应用程序页面上的脚本引用被执行(因为它现在在同一来源上),JSON 对象作为方法的值返回。一切完成后,脚本标签将从页面中删除,您的本地变量将持有回调方法的值。例如,假设我执行了以下 JSONP 请求
http://travel.contoso.com/alerts/medical-alerts.ashx?callback=viewAlerts
这将导致浏览器解释以下输出
viewAlerts([{ "ID": 1, "Title": "Flu strikes Kenya" }]);
ChiliBook.lineNumbers = true;ChiliBook.automatic = true;
如果这听起来像脚本注入,那么你是对的。为了绕过同源策略的限制,JSONP 允许你创建一个到另一个源的脚本引用,然后在你的应用程序的源中执行该脚本,并得到一个与你的应用程序同源的值。可以想象,这为你的应用程序带来了重大的安全风险。作为最佳实践,请将 JSONP 的使用限制在受信任的提供商,并且仍然要对 JSON 输出进行清理处理。不幸的是,一些提供商只提供 JSONP 数据,如果你想访问该提供商的数据,就必须接受这个风险。请咨询你的信息安全团队关于 JSONP(或使用外部脚本资源)的政策和程序。在你的威胁模型中记录 JSONP 的实现,并说明其必要性。
许多人正在集思广益,想出一些巧妙的方法来使 JSONP 更安全(例如,在 iframe 沙箱中执行它和标准化 JSON-P 的调用方式),但在 JSONP 更加成熟之前,它对您的应用程序构成安全风险。
XML
另一种常见的接收数据格式是 XML。与 JSON 一样,如果您使用 jQuery 进行 Ajax 请求并指定数据类型为 XML,您将根据浏览器的设置收到一个 XML 文档。jQuery 提供了一些很好的帮助函数来检查 XML 数据并将其解析为有效的 XML 文档
if (null === suppliedDataType) { if (jQuery.isXMLDoc(that.options.data)) { suppliedDataType = "xml"; output = that.options.data; } else { output = $.parseXML(that.options.data); if (null !== output) suppliedDataType = "xml"; } }
我们有可能请求了 `dataType: "xml"`,并且 Ajax 请求返回了一个 XML 文档。测试这种情况是第一步。通过使用 `jQuery.isXMLDoc` 方法,您可以避免浏览器中 XML 的混乱(因为不同的浏览器处理方式不同)。如果数据已经是格式良好的 XML 文档,此方法将返回 True。如果它还不是 XML 文档,代码会尝试使用 `$.parseXML` 方法将响应转换为 XML。使用 `parseXML` 方法,jQuery 会检查 `that.options.data` 并确定它是否可以转换为 XML 文档。如果数据不是 XML,`parseXML` 方法将返回一个空值。成功将 `that.options.data` 转换为 XML 后,我们将 `suppliedDataType` 设置为 XML。
其他数据类型呢?
在我们的应用程序中,我们只接受 JSON 和 XML。任何其他类型的数据都将被拒绝。用输入验证的术语来说:我们 `约束` 输入为 JSON 和 XML,并 `拒绝` 所有其他数据类型。如果我们收到文本,它需要能够被转换为 JSON 或 XML,否则它将被忽略。这个限制让我们能够减少流经我们系统的数据种类。随着我们采纳更多的数据类型,我们可以扩展解决方案,但目前我们只需要关注“已知的好数据”。图 2 显示了我们的拒绝过程。
switch (suppliedDataType) { case "json": this._recursiveJSONSanitizer(output, this._sanitizeJSON); break; case "xml": this._recursiveXMLNodeSanitizer(output.firstChild, this._sanitizeXML); break; default: throw "The supplied data type is either not supported or not recognized."; break; } return output; },
清理:使数据安全且不可执行
现在我们已经对有效的数据类型设置了约束并拒绝了所有其他类型,我们需要清理从 Ajax 请求中得到的内容。这个清理过程需要几个子过程
- 规范化我们的数据
- 确保注入的代码被渲染为无效
- 确保只有允许的 HTML 可用
在这里,我们将研究 sanitizeJSON
方法以及它如何执行这些步骤。sanitizeXML
和 sanitizeJSON
方法的工作方式相同。它们从一个递归方法中调用,该方法遍历每个节点以处理对象的文本
_sanitizeJSON: function (data) { var tainted_data = data; var canon_data = $.encoder.canonicalize(tainted_data); if (null != window.toStaticHTML) data = window.toStaticHTML(canon_data); else data = $.encoder.encodeForHTML(canon_data); },
首先,我们将数据捕获在一个“受污染的”变量中。这只是为了向开发人员表明该数据尚未经过清理过程。接下来,我们使用 Chris Schmidt 的 $.encoder 插件检查数据是否存在多种编码。如果 `canonicalize` 命令检测到多种编码,它会抛出一个异常,因为多种编码的存在是安全威胁的标志。然后,我们使用 `window.toStaticHTML` 命令使 `tainted_data` 中的任何脚本失效。`toStaticHTML` 函数是 Internet Explorer 内置的 JavaScript 函数,可以防止恶意脚本在数据通道中执行。如果您针对其他浏览器,可以使用 `$.encoder.encodeForHTML(canon_data)` 方法达到类似的效果。最后,如果您有允许的 HTML 标签列表,您可以使用一个简单的替换调用,将编码后的 HTML 替换为解码后的 HTML 标签,如下所示
data = window.toStaticHTML(canon_data).replace(‘<p>’, "<p>").replace("</p>", "</p>");
如果您允许呈现标签,请在您的威胁模型中维护一个可接受标签的列表。安全团队需要了解这些信息。某些 HTML 标签被认为比其他标签更安全,例如格式化标签,如 <strong>、<em> 和 <blockquote>。请警惕以下标签(列表来自 http://msdn.microsoft.com/en-us/library/ff649310.aspx),因为它们已知被用于向您的应用程序注入脚本或其他恶意代码
- <applet>
- <body>
- <embed>
- <frame>
- <script>
- <frameset>
- <html>
- <iframe>
- <img>
- <style>
- <layer>
- <link>
- <ilayer>
- <meta>
- <object>
我没有涵盖清理小部件的所有方面。清理小部件的完整代码可以在 MSDN 代码库中找到。这是一个框架,我鼓励您在其基础上进行构建并提交您的改进。
现在我们将关闭这个小部件
} } (this.hybrid = this.hybrid || {}, jQuery));
然后将其用作 Project Silk 的数据管理器中 `sendRequest` 方法的一部分,如图 3 所示。
sendRequest: function (options) { var that = hybrid.dataManager; var cachedData = hybrid.dataStore.get(options.url); var callerOptions = $.extend({ cache: options.cache }, that.dataDefaults, options); if (callerOptions.cache && cachedData) { var sanitized_cacheData = hybrid.sanitizer.sanitize({ data: cachedData, dataType: callerOptions.dataType }); options.success(sanitized_cacheData); return; } callerOptions.success = function (data) { var tainted_data = data; //setup the data as a tainted component that needs //sanitization var sanitized_data = hybrid.sanitizer.sanitize({ data: tainted_data, dataType: callerOptions.dataType }); if (callerOptions.cache) { hybrid.dataStore.set(callerOptions.url, sanitized_data); } options.success(sanitized_data); }; $.ajax(callerOptions); },
评论主题
在聚合应用的世界里,“所有输入都是邪恶的”这句箴言还适用吗?如果你不信任构成应用程序的所有数据和功能,你还能构建出一个出色的应用程序吗?
约束、拒绝和清理
本文重点讨论输入验证的活动。我首先讨论了信任及其在聚合应用领域中的作用。然后,我们使用 Project Silk 的数据管理器,研究了一个可以用来对从 Ajax 请求返回的数据执行输入验证三要素的小部件。这是保护聚合应用用户的深度防御策略中的又一层。在下一篇也是最后一篇文章中,我将探讨可能是最强大的防线,即现代浏览器内置的安全功能,如 XSS 过滤、安全模式以及针对不仅仅是 OWASP Top Ten 的防御措施。
关于作者
在过去十年中,Tim 一直在使用 JavaScript、ASP.NET 和 C# 构建 Web 应用程序。现在,Tim 领导 FrontierMEDEX 的开发团队,构建在线医疗和安全情报工具。Tim 是一名 CISSP 和 CEH,专注于安全软件开发。
本文由 Tim Kulp 撰写。在过去十年中,Tim 一直在使用 JavaScript、ASP.NET 和 C# 构建 Web 应用程序。现在,Tim 领导 FrontierMEDEX 的开发团队,构建在线医疗和安全情报工具。Tim 是一名 CISSP 和 CEH,专注于安全软件开发。
在以下平台找到 Tim