Cloudflare解析器bug導致內(nèi)存泄漏事件報告
一、前言
上星期五,來自谷歌Project Zero組織的Tavis Ormandy聯(lián)系Cloudflare,報告了我們的邊界服務器的一個安全問題。他看到經(jīng)過Cloudflare的一些HTTP請求返回了崩潰的網(wǎng)頁。
它出現(xiàn)在一些不尋常的情況下,在下面我將詳細介紹,我們的邊界服務器運行時緩沖區(qū)越界了,并返回了隱私信息,如 HTTP cookies,認證令牌,HTTP POST體和其他敏感數(shù)據(jù)的內(nèi)存。并且有些數(shù)據(jù)會被搜索引擎緩存。
為了避免懷疑,Cloudflare客戶的SSL私鑰沒有泄漏。Cloudflare總是通過一個隔離的Nginx實例來結束SLL連接,因此不受這個bug影響。
我們快速的確認了這個問題,并關閉了3個Cloudflare功能(郵件混淆,服務端排除和自動HTTPs重寫),這些都用來了相同的HTML解析器鏈,會導致泄漏。這樣在一個HTTP響應中就不會有內(nèi)存返回了。
因為這個bug的嚴重性,來自San Francisco和 London的軟件工程師、信息安全和操作的交叉功能團隊充分了解了潛在的原因,為了降低內(nèi)存泄漏的影響,和谷歌和其他搜索引擎團隊一起將緩存的HTTP響應移除了。
擁有一個全球化的團隊,每12小時為間隔,每天24小時交替解決這個問題。團隊持續(xù)的努力確保了問題的圓滿解決。作為服務的一個優(yōu)勢是這個bug從報告到解決,花了幾分鐘到幾小時,而不是幾個月。針對這樣的bug部署修復方案的工業(yè)標準通常是3個月;我們在小于7個小時就圓滿解決,47分鐘內(nèi)就緩解了bug。
這個bug是嚴重的,因為泄漏的內(nèi)存包含了隱私信息,并且還會被搜索引擎緩存。我們還沒有發(fā)現(xiàn)這個bug的漏洞利用或者它們存在的報告。
影響最大的時期是2月13日到2月18號,通過Cloudflare的每3,300,000個HTTP請求中約有1個可能導致內(nèi)存泄漏(約為請求的0.00003%)。
我們感謝它由世界頂級安全研究團隊發(fā)現(xiàn)并報告給我們。
本文很長,但是作為我們的傳統(tǒng),我們傾向于對我們的服務出現(xiàn)的問題保持開放和技術上的詳細描述。
二、運行時解析并修改HTML
很多Cloudflare服務依賴通過我們的邊界服務器時解析和修改HTML頁面。例如,我們能通過修改HTML頁面來插入谷歌分析標簽,安全的重寫http://鏈接為https://,排除來自機器人的部分頁面,模糊電子郵件地址,啟用AMP等。
為了修改頁面,我們需要讀取并解析HTML以發(fā)現(xiàn)需要修改的元素。因為在Cloudflare的早期,我們已經(jīng)使用了用Ragel編寫的解析器。一個獨立的.rl文件包含一個HTML解析器,被用來在Cloudflare平臺修改HTML。
大約一年前,我們認為Ragel解析器維護起來太復雜,并且我們開始寫一個新的解析器(cf-html)來替代它。這個解析器能正確處理HTML5,而且非常非??烨乙拙S護。
我們首先將這個解析器用于自動HTTP重寫功能,并一直慢慢地遷移cf-html替換Ragel。
Cf-html和老的Ragel解析器都作為Nginx模塊實現(xiàn),并編譯到我們的Nginx構建中。這個Nginx過濾模塊解析包含HTML響應的緩沖區(qū)(內(nèi)存塊),做出必要的修改,并將緩沖區(qū)傳遞給下一個過濾模塊。
這樣,引起內(nèi)存泄漏的bug在我們的Ragel解析器中已存在多年,但是因為我們內(nèi)部Nginx使用緩沖區(qū)的方式,并沒有內(nèi)存泄漏。Cf-html巧妙的改變了緩沖去,導致在cf-html中不會有問題。
因為我們知道了這個bug是由激活cf-html引起的(但是之前我們知道為什么),我們禁用了使用它的3個功能。Cloudflare每個功能都有一個相應的功能標志,我們稱之為全局殺手。我們在收到問題細節(jié)報告后的47分鐘時啟用了郵件混淆的全局殺手,并在3小時5分鐘后關閉了自動HTTP重寫。郵件混淆功能在2月13號已經(jīng)修改了,并且是內(nèi)存泄漏的原因,因此禁用它快速地阻止了幾乎所有的內(nèi)存泄漏。
在幾秒內(nèi),這些功能在全球范圍內(nèi)被禁用。我們確定沒有通過測試URI來泄漏內(nèi)存,并且谷歌的二次校驗也一樣。
然后,我們發(fā)現(xiàn)了第三個功能(服務端排除)也有這個漏洞,但是沒有全局殺手開關(它非常老,在全局殺手之前實現(xiàn))。我們?yōu)榉斩伺懦龑崿F(xiàn)了一個全局殺手,并全球部署補丁。從發(fā)現(xiàn)服務端排除是個問題到部署補丁只花了3個小時。然而,服務端排除很少使用,且只針對對惡意的IP地址才激活。
三、bug的根因
Ragel代碼轉化為C代碼,然后編譯。這個C代碼使用經(jīng)典的C方法,指向HTML文檔的指針被解析,并且Ragel自身給用戶提供了針對這些指針大量的控制權。因為一個指針錯誤導致的bug的產(chǎn)生。
- /* generated code */
- if ( ++p == pe )
- goto _test_eof;
bug的根因是,使用等于運算符來校驗是否到達緩沖區(qū)的末端,并且指針能夠步過緩沖去末端。這是熟知的緩沖去溢出。使用>=代替==來做檢驗,將跳過緩沖區(qū)末端。這個等于校驗由Ragel自動生成,不是我們編寫的代碼。意味著我們沒有正確的使用Ragel。
我們編寫的Ragel代碼包含了一個bug,其能引起指針越界且給了等號校驗造成緩沖區(qū)溢出的能力。
下面是Ragel代碼的一段代碼,用來獲取HTML標簽中的一個屬性。第一行說的是它試圖找到更多以空格,正斜杠或>結尾的unquoted_attr_char。(:>>是連接符)
- script_consume_attr := ((unquoted_attr_char)* :>> (space|'/'|'>'))
- >{ ddctx("script consume_attr"); }
- @{ fhold; fgoto script_tag_parse; }
- $lerr{ dd("script consume_attr failed");
- fgoto script_consume_attr; };
如果一個屬性格式良好,則Ragel解析器跳轉到@{}代碼塊。如果解析屬性失敗(就是我們今天討論的bug的開始),那么到$lerr{}。
舉個例子,在實際情況下(細節(jié)如下),如果web頁面以錯誤的HTML標簽結尾,如:
- <script type=
$lerr{ }塊將執(zhí)行,并且緩沖去將溢出。這個例子中$lerr執(zhí)行dd(“script consume_attr failed”);(這是個調(diào)試語句),然后執(zhí)行fgoto script_consume_attr;(轉移到script_consume_attr去解析下一個屬性)。
從我們的分析中看,這樣錯誤的標簽出現(xiàn)在大約0.06%的網(wǎng)站中。
如果你觀察仔細,你可能已經(jīng)注意到@{ }也是一個fgoto,但是在它之前執(zhí)行了fhold,并且$lerr{ }沒有。它沒有fhold導致了內(nèi)存泄漏。
在內(nèi)部,生成的C代碼有一個指針p,指向HTML文檔中正在檢測的字符。Fhold等價于p--,并且是必要的。因為當錯誤條件發(fā)生時,p將指向導致script_consume_attr失敗的字符。
并且它非常重要,因為如果這個錯誤條件發(fā)生在包含HTML文檔的緩沖區(qū)的末尾,則p將在文檔末端的后面(p將是pe+1),且達到緩沖區(qū)末尾的校驗將失敗,p將在緩沖去外部運行。
添加一個fhold到錯誤處理函數(shù)中,能解決這個問題。
四、為什么
上面解釋了指針如何運行超過緩沖區(qū)的末尾,但是問題內(nèi)部為什么沒有顯示。畢竟,這個代碼在生產(chǎn)環(huán)境上已經(jīng)穩(wěn)定很多年了。
回到上面定義的script_consume_attr:
- script_consume_attr := ((unquoted_attr_char)* :>> (space|'/'|'>'))
- >{ ddctx("script consume_attr"); }
- @{ fhold; fgoto script_tag_parse; }
- $lerr{ dd("script consume_attr failed");
- fgoto script_consume_attr; };
當解析器解析超過字符范圍時會發(fā)生什么,當前被解析的緩沖區(qū)是否是最后一個緩沖區(qū)是不同的。如果它不是最后一個緩沖區(qū),那么沒必要使用$lerr,因為解析器不知道是否會發(fā)生錯誤,因為剩余的屬性可能在下一個緩沖區(qū)中。
但是如果這是最后一個緩沖區(qū),那么$lerr被執(zhí)行。下面是代碼末尾如何跳過了文件末尾且運行內(nèi)存。
解析函數(shù)的入口點是ngx_http_email_parse_email(名字是古老的,它不止做了郵件解析的事)。
- ngx_int_t ngx_http_email_parse_email(ngx_http_request_t *r, ngx_http_email_ctx_t *ctx) {
- u_char *p = ctx->pos;
- u_char *pe = ctx->buf->last;
- u_char *eof = ctx->buf->last_buf ? pe : NULL;
你能看到p指向緩沖區(qū)的第一個字符,pe是緩沖區(qū)后的字符,且pe設置為eof。如果這是這個鏈中的最后一個緩沖區(qū)(有boolean last_buf表示),否者為NULL。
當老的和新的解析器在請求處理時同時存在,這類緩沖區(qū)將傳給上面的函數(shù)。
- (gdb) p *in->buf
- $8 = {
- pos = 0x558a2f58be30 "<script type=\"",
- last = 0x558a2f58be3e "",
- [...]
- last_buf = 1,
- [...]
- }
上面是數(shù)據(jù),last_buf是1。當新的解析器不存在時,最后一個緩沖區(qū)包含的數(shù)據(jù)如下:
- (gdb) p *in->buf
- $6 = {
- pos = 0x558a238e94f7 "<script type=\"",
- last = 0x558a238e9504 "",
- [...]
- last_buf = 0,
- [...]
- }
最后的空緩沖區(qū)(pos和last都是NULL且last_buf=1)接著那個緩沖區(qū),但是如果緩沖區(qū)是空的,ngx_http_email_parse_email不會被調(diào)用。
因此,只有當老的解析器存在是,最后一個緩沖區(qū)包含的數(shù)據(jù)才有l(wèi)ast_buf是0。這意味著eof將是NULL。現(xiàn)在當試圖在緩沖區(qū)末尾處理一個不完整的script_consume_attr。$ lerr將不會被執(zhí)行,因為解析器相信(因為last_buf)可能有更多的數(shù)據(jù)來了。
當兩個解析器都存在時,情況是不同的。 last_buf為1,eof設置為pe,$ lerr代碼運行。下面是生成的代碼:
- /* #line 877 "ngx_http_email_filter_parser.rl" */
- { dd("script consume_attr failed");
- {goto st1266;} }
- goto st0;
- [...]
- st1266:
- if ( ++p == pe )
- goto _test_eof1266;
解析器解析完字符,而試圖執(zhí)行script_consume_attr, p將是pe。因為沒有fhold(這將做p--),當代碼跳轉到st1266, p增加將超過pe。
然后不會跳轉到_test_eof1266(在這將執(zhí)行EOF校驗),并且將超過緩沖區(qū)末尾,試圖解析HTML文檔。
因此,bug潛伏了多年,直到在NGINX過濾器模塊之間傳遞的緩沖區(qū)的內(nèi)部風水隨著cf-html的引入而改變。
五、繼續(xù)尋找bug
在1960和1970年代,IBM的研究展示了bug集中在易錯模塊中。因為我們在Ragel生成的代碼中找到了一個討厭的指針溢出,所以將謹慎的去查找其他的bug。
信息安全團隊的一部分人開始模糊測試生成的代碼,來查找潛在的指針溢出。另一個團隊使用惡意構造的web網(wǎng)頁構建測試用例。軟件工程師團隊開始手動排查代碼問題。
決定在生成的代碼中為每個訪問的指針顯式添加校驗,并且計入任何發(fā)生的錯誤。生成的錯誤被反饋到我們的全局錯誤記錄基礎結構,用于分析。
- #define SAFE_CHAR ({\
- if (!__builtin_expect(p < pe, 1)) {\
- ngx_log_error(NGX_LOG_CRIT, r->connection->log, 0, "email filter tried to access char past EOF");\
- RESET();\
- output_flat_saved(r, ctx);\
- BUF_STATE(output);\
- return NGX_ERROR;\
- }\
- *p;\
- })
看到日志如下:
- 2017/02/19 13:47:34 [crit] 27558#0: *2 email filter tried to access char past EOF while sending response to client, client: 127.0.0.1, server: localhost, request: "GET /malformed-test.html HTTP/1.1”
每行日志表示一個HTTP請求,可能有泄漏的內(nèi)存。通過記錄問題發(fā)生的頻率,我們希望得到在錯誤存在時HTTP請求泄漏內(nèi)存的次數(shù)的統(tǒng)計。
為了針對內(nèi)存泄漏,下面的東西必須正確:
- 最后一個緩沖區(qū)包含的數(shù)據(jù)必須以以惡意格式的腳本或者img標簽結束
- 緩沖區(qū)必須小于4K長度(否則Nginx可能崩潰)
- 用戶必須開啟郵件混淆(因為她同時使用新舊解析器)。
…或者自動HTTPs重寫/服務端排除(使用了新解析器)組合另一個使用老的解析器的功能。…并且服務端排除只有在客戶端IP具有較差的信譽(即它對大多數(shù)訪問者不起作用)時才執(zhí)行。
那就解釋了為什么緩沖區(qū)溢出導致了內(nèi)存泄漏的發(fā)生的情況如此少。
此外,郵件模糊功能(使用兩個解析器,并會使錯誤發(fā)生在大多數(shù)Cloudflare網(wǎng)站上)僅在2月13日(Tavis報告的前四天)啟用。
涉及的三個功能按如下順序推出。內(nèi)存可能泄漏的最早的日期是2016-09-22。
- 2016-09-22 Automatic HTTP Rewrites 啟用
- 2017-01-30 Server-Side Excludes 整合新的解析器
- 2017-02-13 Email Obfuscation 部分整合新的解析器
- 2017-02-18 Google 報告問題給Cloudflare且泄漏結束
最大的潛在威脅發(fā)生在2月13號開始的4天內(nèi),因為自動HTTP重寫沒有被廣泛使用,服務端排除只對惡意的IP地址才有效。
六、Bug的內(nèi)部影響
Cloudflare在邊界機器上面運行了多個獨立的進程,且提供了進程和內(nèi)存隔離。內(nèi)存泄漏來自與一個基于Nginx的進程(處理HTTP)。它有一個獨立的進程堆處理SSL,圖片壓縮和緩存,意味著我們很很快判斷我們客戶的SSL私鑰沒有泄漏。
然而,內(nèi)存空間的泄漏包含了敏感信息。泄漏的信息中明顯的一個是用于在Cloudflare機器之間安全連接的私鑰。
當處理客戶網(wǎng)站HTTP請求時,我們的邊界機器在機架內(nèi),在數(shù)據(jù)中心內(nèi),以及用于記錄,緩存和從源Web服務器檢索網(wǎng)頁的數(shù)據(jù)中心之間相互通信。
為了響應對互聯(lián)網(wǎng)公司的監(jiān)控活動的高度關注,我們決定在2013年加密Cloudflare機器之間的所有連接,以防止這樣的攻擊,即使機器坐在同一機架。
泄漏的私鑰是用于此機器加密的私鑰。在Cloudflare內(nèi)部還有少量的密鑰用于認證。
七、外部影響和緩存清除
更關心的事實是大量的Cloudflare客戶的HTTP請求存在于轉儲的內(nèi)存中。這意味著隱私信息泄露了。
這包括HTTP頭,POST數(shù)據(jù)(可能包含密碼),API調(diào)用的JSON,URI參數(shù),Cookie和用于身份認證的其他敏感信息(例如API密鑰和OAuth令牌)。
因為Cloudflare運行大型共享基礎架構,因此對易受此問題影響的Cloudflare網(wǎng)站的HTTP請求可能會泄露不相關的其他Cloudflare站點的信息。
另一個問題是,Google(和其他搜索引擎)通過其正常的抓取和緩存過程緩存了一些泄漏的內(nèi)存。我們想要確保在公開披露問題之前從搜索引擎緩存中清除這些內(nèi)存,以便第三方無法搜索敏感信息。
我們傾向是盡快得到錯誤的消息,但我們認為我們有責任確保搜索引擎緩存在公開宣布之前被擦除。
信息安全團隊努力在搜索引擎緩存中識別已泄漏內(nèi)存并清除內(nèi)存的URI。在Google,Yahoo,Bing和其他人的幫助下,我們發(fā)現(xiàn)了770個已被緩存且包含泄漏內(nèi)存的獨特的URI。770個獨特的URI涵蓋161個唯一域。泄漏的內(nèi)存已經(jīng)在搜索引擎的幫助下清除。
我們還進行其他搜索,尋找在像Pastebin這樣的網(wǎng)站上可能泄漏的信息,且沒有找到任何東西。
八、一些課題
新的HTML解析器的工程師一直擔心影響我們的服務,他們花了幾個小時來驗證它不包含安全問題。
不幸的是,這是一個古老的軟件且包含一個潛在的安全問題,而這個問題只出現(xiàn)于我們在遷移拋棄它的過程。我們的內(nèi)部信息安全團隊現(xiàn)在正在進行一個項目,以模糊測試舊軟件的方式尋找潛在的其他安全問題。
九、時間點細節(jié)
我們非常感謝Google的同事就此問題與我們聯(lián)系,并通過其解決方案與我們密切合作。所有這些都沒有任何報告,表明外界的各方已經(jīng)確定了問題或利用它。
所有時間均為UTC時間。
- 2017-02-18 0011 來自Tavis Ormandy的推特尋求Cloudflare的聯(lián)系方式
- 2017-02-18 0032 Cloudflare 收到來自谷歌的bug細節(jié)
- 2017-02-18 0040 多個團隊匯集在San Francisco
- 2017-02-18 0119 全球范圍內(nèi)關閉郵件混淆功能
- 2017-02-18 0122 London團隊加入
- 2017-02-18 0424 自動HTTPs重寫功能關閉
- 2017-02-18 0722 實現(xiàn)針對cf-html解析器的關閉開關,并全球部署
- 2017-02-20 2159 SAFE_CHAR 修復部署
- 2017-02-21 1803 自動HTTPs重寫,服務端排除和郵件混淆重啟功能