导语:在圣诞节之后,我与@mikewest和@fgrx讨论了关于使用“隔离”的概念(将会在下面解释)作为对抗XSS漏洞攻击的安全缓解措施。
概述
在圣诞节之后,我与@mikewest和@fgrx讨论了关于使用“隔离”的概念(将会在下面解释)作为对抗XSS漏洞攻击的安全缓解措施。这看起来似乎是一个很有趣的问题,因此,我花了一些时间研究了一下。
下面描述的设计方案将允许你(web开发人员)通过两个简单的步骤保护一些客户端免受XSS攻击:
设置新的cookie标志(isolatedScript = true) 设置一个HTTP标头(Isolated-Script:true)
就这样。有了这两个东西,你就可以保护你的应用程序防御XSS漏洞攻击。它有点类似于Suborigins,但是使用的是“隔离脚本”的思路防御XSS攻击,即使js文件是在同一个页面内!
除此之外,它还有另外一个优点,它也能够缓解第三方JavaScript代码(如广告,分析,etcetera)。
设计
隔离脚本包括以下三个要素,它们一起工作以提供隔离脚本的防御能力。
(译者注:为了让读者更易理解本文所提供的思路且保证原文纯正的feel,所以译者对这三个要素名词不作翻译)
Isolated Worlds Isolated cookies Secret HTML
我会在下面详细描述每一个要素,并给你一些演示。
Isolated Worlds是当前在Chrome扩展的用户脚本和Firefox中的Greasemonkey脚本中最突出使用的一个概念 —— 本质上,它允许JavaScript代码通过文档的DOM获得“view(视图)”,但是是在一个被隔离的JavaScript运行时环境中。 让我详细的来解释一下:
假设在一个网站上有如下代码:
Isolated World将有权访问document.getElementById(” text”),但它不能够访问window.variable这个变量。 也就是说,两个脚本共享的唯一的东西就是HTML的DOM的独立视图。 这种隔离非常重要,因为用户脚本可能会提升权限,例如,他们可以向任何网站发起XMLHttpRequests请求并读取其响应内容。
如果不使用Isolated World进行隔离,那么页面就可以在用户的脚本中执行代码,并最终攻击用户:
document.getElementById = function() { arguments.callee.caller.constructor("attack()")(); };
通过这样的方式,Isolated World就允许我们从攻击者执行的上下文中保护我们的特权执行上下文。那么,问题来了:我们能否可以使用相同的设计来保护受信任的脚本防止XSS漏洞吗?
也就是说,我们与其专注于阻止脚本执行作为一种缓解措施(我们发现这种措施容易出问题)还不如将关注点放在将可信脚本与不可信脚本隔离开来。
在同一个DOM中运行具有特权和非特权脚本的想法并不新鲜,事实上,已经有一些这方面的具体实现(如Mario Heiderich的Iceshield和Antoine Delignat-Lavaud的Defensive JS),但是它们的实现都需要将代码重写。使用Isolated World,就没有那么麻烦。
因此,隔离脚本的思路就是—— 为Web开发人员提供一种机制,将特定脚本标记为受信任的,然后由浏览器将它们加载后在Isolated World中运行。
要在demo中实现这一点的一个简单方法就是通过在Chrome扩展中重用Isolated Worlds实现,通过在页面中插入一个用户脚本,为每个脚本设置正确的HTTP响应头。
(译者注:被隔离的脚本就是可信任的脚本,而不是恶意脚本文件。)
Isolated Cookies
现在我们有一个脚本运行在受信任的执行上下文中,现在我们需要一种方法能够让Web服务器识别来自这个脚本的请求。这个做法很有必要,因为服务器可能只想将一些敏感数据暴露给被隔离的脚本。
最简单的方法是添加一个类似于Origin头的新的HTTP请求头(我们可以使用Isolated-Script:foo.js)。另一种方法是创建一种新的cookie类型,它只在请求来自于一个被隔离的脚本时才会发送给服务器。此替代方法优于HTTP请求头有两个原因,如下:
向后兼容,没有实现这种方法的浏览器将只是像往常一样发送cookie。 它可以与同一网站的Cookie(同时也能防御CSRF)结合使用。
Web服务器需要设置Cookie信息:
Set-Cookie: SID=XXXX; httpOnly; secure; SameSite=Strict; isolatedScript
然后,浏览器会像往常一样处理cookie,只有当请求是由被隔离的脚本发出的才会在请求信息中包含该Cookie,另外,如果浏览器不能识别这个新的标志,将会在所有的请求信息中包含该Cookie。
要在demo中实现这一点的一个简单方法是,在cookie中使用特殊的名称,并且除非请求来自于被隔离的脚本否则拒绝发送cookie。
Devdatta提出了一个想法是使用cookie前缀,这同样也可以保护cookie免受窜改攻击。
Secret HTML
我们现在的这种针对脚本隔离的机制依旧让脚本有一些额外的特权,并让这些脚本在隔离的执行上下文中运行来阻止恶意代码执行,然而,该脚本如果将想要显示给用户的数据写入到 DOM后,那么恶意的脚本也能够立即读取到这些内容。所以,为此,我们需要为将被隔离的脚本读写的HTML 内容进行保护从而让恶意的脚本不能够读取到。
虽然此原语你可能从未听过,但它实际上在今天已经存在了!JavaScript代码本身已经可以编写用户可见的HTML内容,但如何让这些HTML内容对DOM不可用?有至少两种方法可以做到这一点︰
创建 iframe,然后导航到: data:text/html URL (在 Firefox 中不起作用,因为FF会把data: URL 作为同源来看待)。 创建带有沙盒属性但是没有allow-same-origin的iframe (在 Chrome 和 Firefox 都有效)。
所以,总结一下,我们已经可以创建父页面不可访问的HTML节点。唯一的问题是,如何使它易于向后兼容。所以,问题又来了:
CSS不会传递到iframe,是为了解决这个问题,我们可以将计算的样式传递到iframe的body中,注意,这将使得我们确保iframe中的文本看起来和在父页面中一样,但是元素选择器在iframe里面不起作用)。 事件不会传递到父页面,为了解决这个问题,我们可以向iframe中插入一个简单的脚本,将所有事件从iframe转发到父页面的document中。
这样的做法非常类似于隐藏的HTML,而且也没有明显的将信息泄漏给恶意的脚本的结果。
要在demo中实现这个思路的一个简单的方法是在innerHTML上创建一个setter函数,每当被隔离的脚本尝试设置innerHTML的值时,我们就改为创建一个带有沙盒属性的iframe并将内容写到这个iframe中,我们可以使用postMessage 将CSS和HTML传递到iframe。并创建一个可用于将事件传递到iframe的消息通道。为了避免混入其他代码依赖,我们可以在封闭的Shadow DOM中创建此iframe。
Malte提出的这个特性的一个潜在问题是,根据实现,这可能会将开发者的体验搞乱,因为一些脚本很可能认为DOM中的一些元素是可以正常访问到的(例如,通过querySelector,getElementsByTagName或其他代码)。考虑这点非常重要,也可能是最宝贵的教训,像我这样的安全人员在设计一些比较奇怪的限制性的API时,我们也应该问问web开发人员想要在页面做什么事情。
Demo
好了!现在我解释了“脚本隔离”的概念,以及它如何在demo中实现,现在是时候让你在行动中看到它了。
首先,要模拟浏览器的行为,你需要安装一个chrome扩展(不要担心,没有可怕的权限),然后去访问“存在漏洞的网站”,并试图窃取XHR响应的内容!如果你可以窃取成功,那么你将会赢得一个特别的 “Isolated Scripts” ?T恤 :D。
所以,让我们来做吧!
1. 安装Chrome扩展程序
2. 访问脚本隔离PoC网站
这个网站中有XSS的页面无处不在(DOM和反射),我禁用了XSS过滤器,使你的感到更轻松一些。我对于了解突破这个防御思路的不同方法超级有兴趣!
请注意,实现中可能导致的Bug在实际的浏览器实现上不会有问题的,但无论如何请让我知道这些bug,尽管它们不能完全否定这个设计思路,我会修复掉这些bug,使这个设计思路尽可能的接近现实。
如果你需要它,Chrome扩展的源代码在这里,需要更改禁用CSP和启用脚本隔离的代码在这里。
分析
现在,我明显有点偏见,因为我已经在这个想法中投入了一些时间,而且我认为这至少有一些希望和潜力。也就是说,我想对它的价值和影响做一个分析,以避免期望过高。我将使用框架来衡量我在上一篇博文中描述到的Web安全缓解措施(如果你还没有阅读它,不用担心,我将在下面做一些解释)。
合理性
合理性标准:我们在多大程度上限制了问题的影响结果?
在这种情况下,影响结果非常容易衡量(模块在实现上有缺陷)。
使用隔离的Cookie保护的任何私密数据仅暴露给被隔离的脚本。而被隔离的脚本所能接触到的任何数据对于XSS来说都是隐藏的。因此,web开发人员可以决定他所需要的限制程度。
Mario提出了一个有趣的警告,在这种保护措施下,仍然有可能使用XSS进行网络钓鱼攻击,并且很可能会导致一小部分社会工程学攻击成功。
最小化
最小化标准:我们最小化问题的数量有多少?
我们也可以计算这个问题。大多数XSS漏洞利用,包括内容嗅探和基于插件的SOP绕过(甚至一些通用类型的XSS错误!)都可以使用脚本隔离进行缓解防御,但对一些DOM型的 XSS不起作用。
之所以无法通过“脚本隔离”缓解DOM XSS,如Angular模板注入和基于eval()的XSS —— 是因为它们仍然继承了被隔离的脚本的一些能力,这类XSS漏洞的触发点本身就包含在被隔离的脚本中也就是我们受信任的脚本中。
我很想知道还有哪些类型的bug,无法利用脚本隔离这种方法缓解。
替代
替代标准:我们用更安全的替代方法能解决的风险有多少?
我们还可以快速衡量我们用脚本隔离解决当前风险的程度。特别是,这个方法的实际采用操作似乎很简单,尽管它还有一些问题:
1. 托管在CDN中的JavaScript代码不能在 Isolated World中运行。这个是预期中的设计思路,不过可能会限制部署的便利性。解决这个问题的一个简单的方法是使用ES6 import语句。
2. 希望能够访问“Secret HTML”(例如广告)的代码将不能正常执行,并且可能会以意想不到的方式运行失败(注意,在浏览器实现中,实际上给予Secret HTML访问Isolated脚本的权限可能是有意义的,如果可能的话)。
我非常感兴趣听到任何其他问题,包括你认为web开发人员在采用这种方法时的一些想法。
简化
简化标准:我们要解决多少问题,而不是增加复杂性?
一般来说,我们在增加了复杂性的同时也消除了复杂性,决定它们是否能够彼此抵消的想法看起来有点主观。
一方面,我们让所有与Secret HTML的内容进行交互都通过单个渠道进行,从而消除了复杂性。
但另一方面,通过添加了另一个cookie标志和用于脚本执行的新条件,以及新的DOM隔离类型,我们这样做使得web平台更难以被理解。
老实说,我们这样做确实有点复杂了。不过在没有其他缓解措施的情况下,这只是稍微有点复杂,毕竟没有其他的防御办法了。我非常有兴趣听到任何关于这个思路应用于web平台的复杂性的任何争论。