Proton Mail 的开源代码中发现了跨站脚本漏洞。此问题允许攻击者窃取解密的电子邮件并冒充受害者,绕过端到端加密。
攻击者必须发送两封电子邮件,受害者必须查看这两封电子邮件。在某些情况下,如果受害者仅查看电子邮件,攻击就会成功。然而,大多数情况下,受害者都需要点击第二封电子邮件中的链接。
技术细节
在 Web 应用程序中处理用户控制的 HTML 总是会带来跨站脚本 (XSS) 的风险。虽然发件人可能希望设计其消息并包含图像,但其他 HTML 标签(例如)<script>
可能会产生不良影响并危及读者的安全。这对于常规网络邮件服务来说已经很危险了,任何人都可以通过知道用户的电子邮件地址来向用户发送恶意电子邮件。
对于端到端加密和注重隐私的网络邮件程序来说,这甚至更加危险,因为用户对服务更加信任。如果攻击者能够在此类应用程序的上下文中执行任意 JavaScript,他们就有可能窃取解密的电子邮件和私钥、取消用户的匿名性并冒充受害者。
为了避免这一切,网络邮件发送者花费了大量精力来确保恶意 HTML 无法通过。他们中的大多数使用最先进的 HTML 清理程序(例如DOMPurify)来消除任何恶意 HTML。这是非常好的第一步,但即使是经过清理的数据也非常脆弱,处理数据时的细微错误可能会危及整个应用程序的安全。
在以下部分中,我们将解释我们在 Proton Mail 中发现的代码漏洞。我们还将强调现代网络防御机制的重要性,它们如何让攻击者的日子变得更加艰难,以及在适当的条件下如何仍然可以绕过它们。最后,我们研究如何修复这些问题,以及如何避免您自己的代码中出现此类漏洞。
在审核电子邮件 HTML 清理逻辑时,我们注意到以下代码片段在已清理的数据上运行。乍一看似乎很无辜,但存在一个严重缺陷:
packages/shared/lib/sanitize/purify.ts:
此代码旨在将<svg>
电子邮件中的元素替换为<proton-svg>
元素。它通过创建一个新元素,移动所有子元素,然后替换旧元素来实现这一点。既然这些元素的内容或属性没有被修改,这怎么可能与安全相关呢?要理解这一点,我们首先需要了解HTML 中的内容。
HTML 有自己的解析规则,它可以包含具有不同解析规则的东西,例如MathML和SVG。它们看起来与 HTML 类似,因为它们也是从 XML 派生的,但是在如何解析它们方面存在一些关键差异,这对于清理人员来说非常重要。
HTML 和 SVG 之间差异的一个例子是元素<style>
。在 HTML 中,此元素包含原始文本,直到下一个结束</style>
标记。在 SVG 中,它包含子元素。当消毒剂在错误的环境下运行时,它可能会做出错误的决定。
这正是 Proton Mail 的案例中所发生的情况。清理程序首先看到一个 SVG 元素,并根据 SVG 上下文清理其子元素。之后,外部<svg>
标签被重命名为<proton-svg>
。由于这不是标准 HTML 或 SVG 标签,因此它会退回到 HTML 上下文中。这会导致浏览器解析结果的方式与清理期间不同!
攻击者可以通过以下有效负载滥用此解析器差异:
sanitizer 将正确识别 SVG 上下文并将元素的内容解析<style>
为<a>
元素。字节序列</style>
隐藏在元素alt
的标签内<a>
,并且不会关闭<style>
元素。由于<img>
标记也隐藏在属性内,因此清理程序不会删除onerror
事件处理程序。
将<svg>
元素重命名为时<proton-svg>
,解析结果如下所示:
正如前面所解释的,由于该<proton-svg>
元素属于 HTML 上下文,因此该<style>
元素的解析规则发生了变化。它的内容现在被解析为原始文本,并且字节序列的第一次出现就</style>
终止该元素。这会导致<img>
元素出现,进而onerror
在渲染期间执行处理程序。消毒液被绕过!
幸运的是,这还不能直接允许攻击者执行任意 JavaScript。Proton Mail 有多重防线,而消毒剂只是第一道防线。
第二道防线:Iframe 沙箱
下一个保护是<iframe>
具有sandbox
属性的元素。清理电子邮件的 HTML 后,结果不会直接插入到 Proton Mail 页面本身的 DOM 中,而是插入到 iframe 的 DOM 中。第一个效果是电子邮件中的 CSS 样式等内容不会影响 Proton Mail 的 UI。这使得 iframe 的内容(以红色标记)与页面的其余部分隔离:
另一个好处是能够通过在属性中提供白名单来限制 iframe 内的页面可以执行的操作sandbox
。对于 Proton Mail,iframe 沙箱具有以下指令:
-
allow-same-origin
-
allow-popups
-
allow-popups-to-escape-sandbox
第一个允许嵌入页面能够将 HTML 插入到 iframe 中,但它也支持相反的方式。第二个指令允许打开弹出窗口和新选项卡;例如,当用户单击链接时。第三个指令允许新打开的页面不受 iframe 沙箱的限制,因为沙箱通常会被新页面继承。
然而,Proton Mail 在 Safari 浏览器中打开时添加了第四个指令。在这种情况下,该allow-scripts
指令被添加到白名单中,这意味着攻击者根本不需要绕过沙箱,因为他们只需执行 JavaScript 并访问顶部框架即可。
对于所有其他浏览器,攻击者必须说服受害者单击在新选项卡中打开的链接,从而逃离沙箱并能够访问打开器的父框架:
第三道防线:内容安全政策
最终的防御机制是 Proton Mail 的内容安全策略(CSP)。它限制了可以加载各种资源的来源,包括脚本、图像和样式。在这种情况下,重要的 CSP 指令是:
-
default-src 'self'
-
style-src 'unsafe-inline'
-
img-src https:
-
script-src blob:
第一个指令充当后备,仅允许资源来自页面加载的来源,除非另有指定。接下来的两个指令允许通过 HTTPS 加载内联 CSS 样式和图像,这对于 HTML 电子邮件来说是正常的。最后一个指令允许从 URL 加载脚本blob:
。这是非常不寻常的,并且将是绕过 CSP 的关键。
让我们快速了解一下 blob URL 是什么。它们是可以由任何页面动态创建的临时 URL,如下所示:blob:https://mail.proton.me/8c2a19fa-8dcd-44d1-807c-1c65abef0251
在模式之后blob:
,它从创建它的页面的起源开始,而 URL 的路径是随机的 UUID。要创建 blob URL,页面必须指定内容类型以及浏览器尝试获取它时将返回的内容。页面可以主动撤销 blob URL,但当页面关闭或重新加载时,它们也会被撤销。
制作任意 Blob URL
对于 Proton Mail,blob URL 用于呈现内联附件,例如图像。一般来说,此类附件都有自己的Content-ID
标头,标头的值可在电子邮件的上下文中唯一标识它们。然后可以使用 URL 引用这些附件cid:
,例如在标签src
的属性中<img>
。
当电子邮件包含具有此类cid:
来源的图像标签时,Proton Mail 将查找具有匹配Content-ID
标头的附件。将使用附件的数据和内容类型创建 Blob URL,并且图像的src
属性将替换为新创建的 Blob URL。
我们注意到 Proton Mail 允许内嵌附件的任意内容类型和内容。这将允许攻击者发送 JavaScript 附件而不是图像,并将其引用为<img>
元素的源,从而触发包含 JavaScript 并具有内容类型的 Blob URL 的创建application/javascript
。
攻击者可能会滥用这种内联图像加载机制来制作任意 blob URL 并将其作为脚本加载以绕过 CSP。剩下的唯一挑战是如何从图像标签的src
属性中获取创建的 blob URL 并将其用作脚本标签的src
属性。
泄露 Blob URL
这就是 CSP 允许的内联样式和远程图像发挥作用的地方。之前已经有关于如何通过 CSS 从 DOM 泄漏数据(例如属性值和文本)的工作。Pepe Vila和Nathanial Lattimer发现的一种这样的方法使用递归 CSS@import
语句。不幸的是,此技术和其他技术在这里不适用,因为 CSP 不允许从远程服务器加载样式或字体。
由于需要泄漏的值是 blob URL,因此我们可以做出一些假设来简化过程。由于原点始终为https://mail.proton.me
,因此 URL 的开头已知为blob:https://mail.proton.me/
。这只留下由十六进制字符和破折号组成的 UUID,将每个字符的可能性减少到 17 个。
对于@import
泄漏技术,CSS 属性前缀选择器用于增量泄漏属性值。由于 CSP 阻止远程 CSS 导入,因此不可能采用这种增量方法。一种替代方案是为所有可能的属性值创建选择器,但由于可能值的数量为 2 122 ,因此这是不可行的。
然而,还有另一个有用的 CSS 属性选择器:“包含”运算符。它可用于检查属性值是否包含某个子字符串。有了这个,我们可以创建一种与@import
泄漏类似的技术,但我们不是采用增量方法,而是并行泄漏多个部分。
将 URL 分割成更小的块
为此,我们必须将想要泄漏的值分割成可能值较少的较小块。在我们的例子中,我们不会立即泄漏整个 UUID,而是并行泄漏所有 3 字符子字符串。我们首先计算 UUID 的所有有效 3 字符子字符串,从 开始000
,到0-0
,直到fff
。然后,我们为每个子字符串创建一个 CSS 选择器,它会告诉我们该子字符串是否包含在我们想要泄漏的当前 UUID 中。当 CSS 选择器匹配时,我们会使用唯一的 URL 从攻击者服务器请求背景图像。
以下是如何将 blob URL 拆分为重叠的 3 字符块的示例:
这样,攻击者服务器将知道 UUID 包含的所有不同块,但不知道它们的顺序。为了重建正确的 UUID,服务器必须通过从一个块开始并找到一个重叠的块来将其重新拼接在一起。
从该块开始8c2
,攻击者将查找以 开头的任何块c2
,从而找到该块c2a
。从那里他们会寻找以 , 开头的块2a
,依此类推。最后,应该重建完整的 blob UUID,除非有多个块以相同的两个字符开头。
好奇的读者可能想知道为什么我们选择 3 字符块而不是其他长度。我们发现 3 是 CSS 大小和冲突概率之间的最佳点,CSS 大小约为 100 KB,而冲突概率低于 1%。
如果我们让每个块只有 2 个字符,我们会减少 CSS 大小,但会大大增加块具有多个可能的后继者的机会,因为块之间的重叠只有 1 个字符。使用更长的块会减少这种可能性,但 CSS 选择器的数量会呈指数级增长。下图显示了 CSS 大小和对数尺度上的碰撞概率之间的权衡:
现在我们有了泄漏 blob URL 的策略,我们需要在 CSS 中实现它。这就是我们遇到问题的地方:我们无法为要泄漏其属性的元素设置多个背景图像,因为它们会相互覆盖。
每个元素多个请求:cross-fade()
解决方案是寻找一种方法将任意数量的背景图像分配给单个元素,以便浏览器都能获取它们。经过几个小时的阅读 CSS 规范后,我们找到了cross-fade()
CSS 函数。该函数采用两个图像和一个百分比作为参数,然后返回叠加两个图像所产生的图像。图像参数可以指定为url()
s,但它们也可能是由对该cross-fade()
函数的另一次调用产生的!这意味着我们可以嵌套任意数量的cross-fade()
调用,强制浏览器请求url()
该嵌套树底部使用的所有 s。
以下示例显示了该嵌套树的外观。浏览器必须加载图像a.jpg
,然后b.jpg
才能创建最终的交叉淡入淡出图像。浏览器还必须c.jpg
先加载,然后才能将其与其他操作的结果交叉淡入淡出。最终结果是可以指定为元素背景图像的单个图像:
第一部分由 UUID 中存在特定子字符串时匹配的所有块选择器组成。他们每个人都将一个 CSS 变量设置为浏览器应获取的 URL,以向攻击者服务器发出此选择器匹配的信号。
最后一个选择器将所有这些 CSS 变量包含在一个大的嵌套调用树中cross-fade()
。当浏览器尝试呈现最后一个选择器时,它必须检查使用的每个变量。对于所有设置的变量,浏览器必须获取引用的 URL 才能使用结果创建最终的交叉淡入淡出图像。
所有未设置的 CSS 变量都将被视为其后备值none
,因此浏览器不会请求任何内容。这就是浏览器网络选项卡中的泄漏情况:
攻击者服务器收到块后,会重建 blob URL 并向受害者发送第二封电子邮件。这次,电子邮件包含一个<script>
使用 blob URL 作为其 的标签src
,以及一个在新选项卡中打开 blob URL 的链接。对于使用 Safari 的受害者来说,脚本标签就足够了,因为不需要绕过 iframe 沙箱。其他受害者将必须单击该链接,这将在新选项卡中打开该链接,从而根据该指令绕过 iframe 沙箱allow-popups-to-escape-sandbox
。
一旦 JavaScript 有效负载被执行,它就可以直接访问运行 Proton Mail 应用程序的顶部窗口。攻击者可以使用此访问权限窃取解密形式的所有电子邮件,以受害者的名义发送电子邮件,甚至可能窃取受害者的加密密钥。
原文始发于微信公众号(TtTeam):Proton Mail 存在XSS漏洞
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论