案例分享:一封邮件如何影响整个邮件服务的解析

译文
安全 数据安全
本文通过作者的亲身经历,分析了Node.js类电子邮件解析器的DoS漏洞,并提出九项有针对性的最佳实践。

【51CTO.com快译】最近业界发现并公布了五大流行Node.js类电子邮件解析器的如下疑似漏洞,这些漏洞都属于隐蔽渐进式的拒绝服务(DoS)类型。

为了利用这些漏洞,可以通过在电子邮件中附上几百万个空的附件,来绕过典型的邮件大小限制(通常为20 MB或更少)。因此,当此类电子邮件被发送到脆弱的邮件服务器上时,由于附件数量过于庞大,所以它会让Node.js的事件循环(event loop)停滞几秒钟。

[[263460]]

同时,由于为每个附件都创建了一个内部对象,因此内存的使用量会马上“爆”到2 GB或者更多,从而让整个服务器因为内存的不足而崩溃。

那么,您真的确信自己的Node.js服务器能安全地解析电子邮件吗?下面,我将通过自己的亲身经历来和大家一起分析并检查电子邮件的解析器。

在开始深入探讨之前,让我们先来欣赏一下由XKCD(由Randall Munroe创作的著名的网络漫画。请参见https://xkcd.com/1873/)带来的“强迫症”漫画。

拒绝服务工具有那么简单吗?

由于依赖项的普及,上述漏洞一旦被利用,就会波及到数千个系统。例如:mailparser库(请参见https://www.npmjs.com/package/mailparser)每月的下载量就多达249,400次,并且目前已被214个其他项目(包括Sendgrid https://www.npmjs.com/package/@sendgrid/inbound-mail-parser)用作依赖项。而Haraka(请参见https://www.npmjs.com/package/Haraka)则是另一个影响深远的库,它正在被Craigslist(请参见https://www.craigslist.org/about/thanks)、Fort Anti-Spam(请参见https://www.fortantispam.com/)和ThreatWave(请参见https://haraka.github.io/users.html)所使用着。

通常,您大可不必请来Cloudflare(译者注:一家提供网站安全管理的公司)的专业服务与帮助,您完全可以自行通过添加一行简单的代码,来修复该问题。例如,您可以通过计算附件(包括那些文本部分)的数量,来验证用户数据的合法性。例如,您可以设定为:如果附件的数量超过1000,则采取丢弃之类的反应动作。

不过,此类修复只是一种治标不治本的被动防御,它在上述五种邮件解析器中的实现方式不尽相同,也不尽完全奏效。因此,我们必须通过如下的改进方式,来真正找到并修补此类漏洞。

想象一下,您正在编写一个邮件解析器......

作为一名开发者,您应该知道有多少份RFC需要阅读和掌握,也应该知道需要编写多少种测试,来确保自己的程序能够符合RFC的相关规定。同时,您也一定听过软件行业内的那句名言:“先让它运行起来,再让它运行得更快。”不过,这一套理论对于电子邮件解析器来说却不那么奏效。就算完成了邮件解析器的编写,您也无法仅仅通过粗略的背板计算(back-of-the-envelope calculation,请参见https://highscalability.com/blog/2011/1/26/google-pro-tip-use-back-of-the-envelope-calculations-to-choo.html),来估算出:在自己的应用中,应当分配多少数量的内存给一个包含了multipart的对象。而且,通过复杂的分析,您还可能发现如下问题:

  • 不止需要测试内存的用量,您还可能需要测量CPU使用率。
  • 您可能无法对那些典型的内存占用量,开展基准化的测试。
  • 由于最终的SMTP应用环境存在着不定性,因此就算90%的电子邮件确实为垃圾邮件,您也无法去启用任何一种快速的解析路径。
  • 您完全可以避免在解析器中执行太多、太严格的策略决策(policy decision)。
  • 您的用户可能并不买账,您对每封邮件采取的大附件数量的限制。
  • 由于不是邮件服务器的管理员,您可能会更愿意在SMTP的事务处理期间,完成了所有内容的解析之后,让用户自行去判断和拒收某些邮件。

想象一下,您正在运行一个电子邮件服务器......

鉴于上述情况,您需要做的事就是:设置邮件的大小限制。显然,邮件越大,它被服务器接收的可能性就越小。因此,为了将解析时间控制在合理范围内,您可以将单封邮件的体积上限限制为20MB。籍此,您就可以放心地使用那些“经过实战考验”的、时下流行的邮件解析器。

为了简化邮件解析器的复杂性,您可以直接将处置20MB电子邮件时的CPU使用率作为参考基线准,以保证自己的服务器能够每秒处理数千封邮件。因此,假设服务器处理相同大小邮件的时间就是恒定的,那么您只需要8GB的内存,便可足够应对每秒200封且大小为20MB的邮件并发量了。有了这样的简化场景,您后续只需要考虑带有0字节的附件,即空文件的安全危害即可。相关概念请参见:https://en.wikipedia.org/wiki/Zip_bomb。下面是一个简单的范例:

  1. MIME-Version: 1.0 
  2. From:  
  3. To:  
  4. Subject: MIME Multipart Attack 
  5. Date: Sat, 30 Jun 2018 15:51:58 +0000 
  6. Message-ID:  
  7. Content-Type: multipart/mixed; boundary="0" 
  8. --0 
  9. Content-Type: text/plain; charset=UTF-8 
  10. Content-Transfer-Encoding: quoted-printable 
  11. --0 
  12. --0 
  13. --0 
  14. --0 
  15. --0 
  16. --0 
  17. --0 
  18. --0 [× 4 million] 

如何发现此类漏洞?

回到开始提到的案例,我曾经遇到过:由于一个奇怪的入栈邮件,突然导致了V8(一种JavaScript引擎)的垃圾收集器一次性地阻止了Node.js的事件循环长达几十秒。我为此花费了两天的时间,每隔几分钟就去重置feature-flags,以减少邮件队列的负荷。在Vyacheslav Egorov(请参见https://github.com/mraleph)的帮助下,我注释掉了V8的CollectAllAvailableGarbage函数(请参见https://github.com/v8/v8/blob/master/src/heap/heap.cc#L1237)。该函数的内部工作原理是:对那些巨大的(几个GB大小)堆栈随机进行七次收集。由此,我所吸取到的教训是:应当谨慎地对堆栈进行对象分配,进而避免对事件循环的阻断。

从去年年初开始,我不断在开源社区--https://github.com/ronomon/mime上编写并更新自己的邮件解析器。我的目标是:希望新的版本能够具有更快的解析速度、更少的资源分配数、能够在原始的缓冲区上运行、以及对RFC具有100%测试覆盖率(包括模糊测试,fuzz tests)。

在此过程中,我进一步了解到:策略决策会比邮件服务器本身更有利于邮件的解析;同时,邮件的解析也会反过来促进策略的决策。此处的策略决策包括:拒绝明显的恶意代码,拒绝各种损坏的、或被截断的Base64、以及Quoted-Printable之类的字符编码,拒绝重复性的关键标题(请参见https://noxxi.de/research/content-transfer-encoding.html),限制multipart的数量,以及限制由于对multipart边界的误报而引起的回溯。

今年初,我与《避免阻断Node.js事件循环的完全指南》(an excellent guide to not blocking the Node.js event loop,请参见https://nodejs.org/en/docs/guides/dont-block-the-event-loop/)一文的作者--Jamie Davis取得了联系。Jamie在文中所讨论的如何抵御事件循环风险,正是我在本文中提及的,针对邮件解析器的multipart风险。

九点改进建议

在此,我为大家列出了针对此类问题的九项值得尝试的实践:

1. DoS对于资源稀缺的系统更容易产生效果。作为知识的积累,您可以通过《mechanical sympathy》(https://mechanical-sympathy.blogspot.com/2011/07/why-mechanical-sympathy.html)一文,来了解底层硬件是如何运作的,以及如何通过编程,实现与底层硬件的良好协作。由于解析的算法既会涉及到CPU的使用,又会涉及到内存的分配,因此我们需要事先合理地配置好硬件资源。如果您的代码能够有效地使用CPU、内存、磁盘、及网络的话,那么您可能就不太会碰到资源匮乏的问题。当然,您仍需要对所有的系统资源,进行合理的使用限制。

2. 在设计之初,就从不同的资源维度进行粗略的背板计算。此法能够尽早地暴露并发现设计中的缺陷,进而避免产生那些“不可能”的解析。由于系统的性能和安全性通常很难通过后期的优化而有所改进,因此它们需要在初期就被规划好,而不要等到用户使用量上去了,才“亡羊补牢”。

3. 平衡所有维度上的资源使用情况。不要出现:您虽然尚有足够的CPU去满足吞吐量的需求,但早已耗尽了内存的情况。因此,您同样需要通过粗略的背面计算,来保持各类资源的使用占比,以避免产生各种设计中的潜在瓶颈。

4. 记住:在运行事件循环时,大多数性能问题都源自拒绝服务式的等待。因此,如果有用户报告性能问题,那么您首要检查的应该就是安全方面的风险可能性。

5. 验证所有用户的入栈数据,不仅要考察单位时间的数据量,还应当检查一段时间的总量。某些风险往往会以潜移默化的方式,对您的邮件系统进行逐步渗透,然后产生倍数效应,并最终接管您的系统。

6. 注意模块边界之间的“空白地带”。不要依赖其他的开发伙伴去帮助您弥补这些不足之处。为了避免策略决策上的缺陷,您应当更好地了解代码间的依赖关系。

7. 从整体的安全角度出发,制定严格的过滤策略,对于不确定是否“健康”的邮件,系统应当坚决“拒之千里之外”。

8. 不要只是从开发人员的角度去检查自己的代码,而需要从恶意击者的角度出发,考虑他们会如何利用那些邮件解析中的代码漏洞。在程序发布之前,请在每个模块中至少仔细地检查并修复三个漏洞。

9. 编写简单的模糊测试用例(请参见https://en.wikipedia.org/wiki/Fuzzing),以随机生成各种有效的和无效的参数。针对某个函数相对其他函数的返回值,请测试其有效性、正确性、以及各种无效的异常输出。您可以根据Linus极端法则(请参见https://en.wikipedia.org/wiki/Linus%27s_Law),运行具有数百万个参数组合的函数模糊测试。

私有与公共披露时间表

该漏洞已于2018年4月23日向受影响模块的所有者进行了披露。不过,就在90天的公开披露截止日期到期之前,该所有者以某种理由推迟了对它的公开披露。此后,通过联系与之相关的依赖项模块(主要是在GitHub上),该漏洞已于2018年6月25日得到了全面公开与披露。

另外,有关这五大Node.js类邮件解析器的DoS漏洞介绍和具体信息,请参见下表:

1. haraka (versions < 2.8.19)

https://snyk.io/vuln/npm:haraka:20180625

  • April 23rd, 2018 - Initial private disclosure to package owner
  • April 24th, 2018 - Initial response from package owner
  • June 15th, 2018 - Vulnerability fixed but not yet published to npm
  • June 25th, 2018 - Public disclosure
  • June 27th, 2018 - Version 2.8.19 published with fix

2. mailparser (ALL versions)

https://snyk.io/vuln/npm:mailparser:20180625

  • April 23rd, 2018 - Initial private disclosure to package owner
  • April 24th, 2018 - Initial response from package owner
  • June 25th, 2018 - Public disclosure

3. emailjs-mime-parser (ALL versions)

https://snyk.io/vuln/npm:emailjs-mime-parser:20180625

  • April 23rd, 2018 - Initial private disclosure to package owner
  • April 24th, 2018 - Initial response from package owner
  • June 25th, 2018 - Public disclosure

4. mailsplit (versions < 4.2.1)

https://snyk.io/vuln/npm:mailsplit:20180625

  • April 23rd, 2018 - Initial private disclosure to package owner
  • April 24th, 2018 - Initial response from package owner
  • June 25th, 2018 - Public disclosure
  • July 23rd, 2018 - Version 4.2.1 published with fix

5. mailparser-mit (ALL versions)

https://snyk.io/vuln/npm:mailparser-mit:20180625

  • April 23rd, 2018 - Initial private disclosure to package owner
  • April 24th, 2018 - Initial response from package owner
  • June 25th, 2018 - Public disclosure

原文标题:How to Crash an Email Server With a Single Email,作者:Liran Tal

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】

 

责任编辑:赵宁宁 来源: 51CTO
相关推荐

2019-05-29 14:16:05

网络安全网络安全技术周刊

2015-12-18 16:46:29

2009-11-01 17:45:07

邮件安全机制网关安全垃圾邮件

2011-11-23 13:24:21

垃圾邮件

2021-01-23 15:01:56

微信QQ移动应用

2009-09-02 14:01:50

邮件服务器

2014-02-13 15:40:03

CIO

2014-06-09 15:43:50

Hadoop

2009-09-15 09:53:41

德国互联网邮件

2012-10-24 16:56:14

广域网加速邮件安全深信服科技

2009-12-15 16:30:02

邮件服务器故障

2018-02-08 09:08:03

大数据 云计算 盘点

2020-01-08 11:42:06

CIO新年祝福

2010-11-04 15:10:48

360公开信

2017-09-22 13:29:42

氚云

2015-05-27 17:28:05

深信服

2011-02-28 16:57:03

Google云服务微软

2009-07-21 16:14:13

2012-11-19 13:06:56

点赞
收藏

51CTO技术栈公众号