WebSocket 做爲 HTML5 的新特性之一格外吸引着開發人員的注意,由於它的出現使得客戶端(主要指瀏覽器)提供對 Socket 的支持成爲可能,從而在客戶端和服務器之間提供了一個基於單 TCP 鏈接的雙向通道。對於實時性要求比較高的應用而言,譬如在線證券、在線遊戲,以及不一樣設備之間信息同步。信息實時同步一直是技術難題,在 WebSocket 出現以前,常看法決方案通常就是輪詢(Polling)和 Comet 技術,但這些技術增長了設計複雜度,也形成了網絡和服務器的額外負擔,在負載較大的狀況下效率相對低下,致使應用的可伸縮行收到制約。對於此類應用的開發者來講,WebSocket 技術簡直就是神兵利器,讀者能夠登錄 websocket.org 網站觀看特點案例,以及它提供的 WebSocket 和 Comet 的性能對比分析報告。最近幾年內 WebSocket 技術被開發人員普遍應用到各種實際應用中。不幸的是,WebSocket 相關的安全漏洞也逐步被披露出來,其中最容易發生的就是跨站點 WebSocket 劫持漏洞。本文將深刻淺出爲讀者介紹跨站點 WebSocket 漏洞的原理、檢測方法和修復方法,但願能幫助廣大讀者在實際工做中避免這個已知安全漏洞。php
爲了便於闡述跨站點 WebSocket 劫持漏洞原理,本文將簡單描述 WebSocket 協議的握手和切換過程。建議有興趣的讀者閱讀參考文獻中提供的 RRFC 6455 規範,深刻學習 WebSocket 協議。html
瞭解過 WebSocket 技術的讀者都知道 ws://和 http://,那麼 WebSocket 和 HTTP 是什麼關係呢。筆者對這個問題的理解是,WebSocket 是 HTML5 推出的新協議,跟 HTTP 協議內容自己沒有關係。WebSocket 是持久化的協議,而 HTTP 是非持久鏈接。正如前文所述,WebSocket 提供了全雙工溝通,俗稱 Web 的 TCP 鏈接,但 TCP 一般處理字節流(跟消息無關),而 WebSocket 基於 TCP 實現了消息流。WebSocket 也相似於 TCP 同樣進行握手鍊接,跟 TCP 不一樣的是,WebSocket 是基於 HTTP 協議進行的握手。筆者利用 Chrome 開發者工具,收集了 websocket.org 網站的 Echo 測試服務的協議握手請求和響應,如清單 1 和 2 所示。java
GET ws://echo.websocket.org/?encoding=text HTTP/1.1 Host: echo.websocket.org Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://www.websocket.org Sec-WebSocket-Version: 13 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_4) Chrome/49.0.2623.110 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6 Cookie: _gat=1; _ga=GA1.2.2904372.1459647651; JSESSIONID=1A9431CF043F851E0356F5837845B2EC Sec-WebSocket-Key: 7ARps0AjsHN8bx5dCI1KKQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
熟悉 HTTP 的朋友能夠發現 WebSocket 的核心了,對的,這就是 Connection:Upgrade 和 Upgrade:websocket 兩行。這兩行至關於告訴服務器端:我要申請切換到 WebSocket 協議。web
HTTP/1.1 101 Web Socket Protocol Handshake Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: content-type Access-Control-Allow-Headers: authorization Access-Control-Allow-Headers: x-websocket-extensions Access-Control-Allow-Headers: x-websocket-version Access-Control-Allow-Headers: x-websocket-protocol Access-Control-Allow-Origin: http://www.websocket.org Connection: Upgrade Date: Sun, 03 Apr 2016 03:09:21 GMT Sec-WebSocket-Accept: wW9Bl95VtfJDbpHdfivy7csOaDo= Server: Kaazing Gateway Upgrade: websocket
一旦服務器端返回 101 響應,便可完成 WebSocket 協議切換。服務器端便可以基於相同端口,將通訊協議從 http://或 https://切換到 ws://或 wss://。協議切換完成後,瀏覽器和服務器端便可以使用 WebSocket API 互相發送和收取文本和二進制消息。跨域
這裏要解釋一些安全相關的重要頭部參數,Sec-WebSocket-Key 和 Sec-WebSocket-Accept。這涉及一個 WebSocket 安全特性,客戶端負責生成一個 Base64 編碼過的隨機數字做爲 Sec-WebSocket-Key,服務器則會將一個 GUID 和這個客戶端的隨機數一塊兒生成一個散列 Key 做爲 Sec-WebSocket-Accept 返回給客戶端。這個工做機制能夠用來避免緩存代理(caching proxy),也能夠用來避免請求重播(request replay)。瀏覽器
細心的讀者可能也注意到不少其餘「Sec-」開頭的 WebSocket 相關的 Header。這其實也是 WebSocket 設計者爲了安全的特地設計,以「Sec-」開頭的 Header 能夠避免被瀏覽器腳本讀取到,這樣攻擊者就不能利用 XMLHttpRequest 僞造 WebSocket 請求來執行跨協議攻擊,由於 XMLHttpRequest 接口不容許設置 Sec-開頭的 Header。緩存
儘管 WebSocket 協議設計時充分考慮了安全保障機制,但隨着 WebSocket 技術推廣,安全工做者們慢慢仍是發現了一些 WebSocket 相關的安全漏洞,譬如 Wireshark 的漏洞 CVE-2013-3562 (Wireshark 1.8.7 以前的 1.8.x 版本中的 Websocket 解析器中的 epan/dissectors/packet-websocket.c 中的‘tvb_unmasked’函數中存在多個整數符號錯誤,遠程攻擊者可經過惡意的數據包利用這些漏洞形成拒絕服務)。Asterisk WebSocket Server 的 DoS 漏洞 CVE-2014-9374(該 WebSocket Server 某模塊中存在雙重釋放漏洞,遠程攻擊者可經過發送零長度的幀利用該漏洞形成拒絕服務)。這兩個 DDoS 漏洞跟 WebSocket 協議自己以及 WebSocket 應用程序相關性不大。但 2015 年來自 Cisco 的 Brian Manifold 和 Nebula 的 Paul McMillan 報告了一個 OpenStack Nova console 的 WebSocket 漏洞(CVE-2015-0259),這個漏洞獲得普遍關注,而且被在不少 WebSocket 應用中發現。事實上,這種漏洞早在 2013 年就被一個德國的白帽黑客 Christian Schneider 發現並公開,Christian 將之命名爲跨站點 WebSocket 劫持 Cross Site WebSocket Hijacking(CSWSH)。跨站點 WebSocket 劫持相對危害較大,也更容易被開發人員忽視。安全
什麼是跨站點 WebSocket 劫持漏洞呢,前文已經說起,爲了建立全雙工通訊,客戶端須要基於 HTTP 進行握手切換到 WebSocket 協議,這個升級協議的過程正是潛在的阿喀琉斯之踵。你們仔細觀察上文的握手 Get 請求,能夠看到 Cookie 頭部把域名下的 Cookie 都發送到服務器端。若是有機會閱讀 WebSocket 協議(10.5 章客戶端身份認證)就發現,WebSocket 協議沒有規定服務器在握手階段應該如何認證客戶端身份。服務器能夠採用任何 HTTP 服務器的客戶端身份認證機制,譬如 cookie,HTTP 基礎認證,TLS 身份認證等。所以,對於絕大多數 Web 應用來講,客戶端身份認證應該都是 SessionID 等 Cookie 或者 HTTP Auth 頭部參數等。熟悉跨站點請求僞造攻擊 Cross Site Request Forgery(CSRF)的朋友到這裏應該就能夠聯想到黑客可能僞造握手請求來繞過身份認證。服務器
由於 WebSocket 的客戶端不只僅侷限於瀏覽器,所以 WebSocket 規範沒有規範 Origin 必須相同(有興趣的讀者能夠閱讀規範 10.2 章節瞭解對於 Origin 的規範)。全部的瀏覽器都會發送 Origin 請求頭,若是服務器端沒有針對 Origin 頭部進行驗證可能會致使跨站點 WebSocket 劫持攻擊。譬如,某個用戶已經登陸了應用程序,若是他被誘騙訪問某個社交網站的惡意網頁,惡意網頁在某元素中植入一個 WebSocket 握手請求申請跟目標應用創建 WebSocket 鏈接。一旦打開該惡意網頁,則自動發起以下請求。請注意,Origin 和 Sec-WebSocket-Key 都是由瀏覽器自動生成,Cookie 等身份認證參數也都是由瀏覽器自動上傳到目標應用服務器端。若是服務器端疏於檢查 Origin,該請求則會成功握手切換到 WebSocket 協議,惡意網頁就能夠成功繞過身份認證鏈接到 WebSocket 服務器,進而竊取到服務器端發來的信息,抑或發送僞造信息到服務器端篡改服務器端數據。有興趣的讀者能夠將這個漏洞跟 CSRF 進行對比,CSRF 主要是經過惡意網頁悄悄發起數據修改請求,不會致使信息泄漏問題,而跨站點 WebSocket 僞造攻擊不只能夠修改服務器數據,還能夠控制整個讀取/修改雙向溝統統道。正是由於這個緣由,Christian 將這個漏洞命名爲劫持(Hijacking),而不是請求僞造(Request Forgery)。websocket
GET ws://echo.websocket.org/?encoding=text HTTP/1.1 Host: echo.websocket.org Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://www.malicious website.com Sec-WebSocket-Version: 13 Accept-Encoding: gzip, deflate, sdch Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6 Cookie: _gat=1; _ga=GA1.2.290430972.14547651; JSESSIONID=1A9431CF043F851E0356F5837845B2EC Sec-WebSocket-Key: 7ARps0AjsHN8bx5dCI1KKQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
讀到這裏,熟悉 JavaScript 跨域資源訪問的讀者可能會懷疑以上觀點。若是 HTTP Response 沒有指定「Access-Control-Allow-Origin」的話,瀏覽器端的腳本是沒法訪問跨域資源的啊,是的,這就是衆所周知的跨域資源共享 Cross-Origin Resource Sharing(CORS),這確實也是 HTML5 帶來的新特性之一。可是很不幸,跨域資源共享不適應於 WebSocket,WebSocket 沒有明確規定跨域處理的方法。
明白跨站點 WebSocket 劫持漏洞原理後,你們就很容易聯想到這個漏洞的檢測方法了,重點就在於重播 WebSocket 協議升級請求。簡單來講就是使用能攔截到 WebSocket 握手請求的工具,修改請求中的 Origin 頭信息,而後從新發送這個請求,看看服務器是否可以成功返回 101 響應。若是鏈接失敗,那麼說明這個 WebSocket 是安全的,由於它能夠正確拒絕來自不一樣源(Origin)的鏈接請求。若是鏈接成功,一般就已經證實服務器端沒有執行源檢查,爲了嚴謹起見,最好進一步測試是否能夠發送 WebSocket 消息,若是這個 WebSocket 鏈接可以發送/接受消息的話,則徹底證實跨站點 WebSocket 劫持漏洞的存在。
爲了便於演示如何測試及修復這個漏洞,筆者編寫了一個簡單的 WebSocket 應用,這個應用基於 JAAS 實現了 HTTP BASIC 身份認證,讀者能夠將這個程序下載部署到 Tomcat 中進行測試。打開客戶端網頁後首先進行登陸,而後點擊「鏈接」按鈕經過 JavaScript 創建 WebSocket 鏈接,而後點擊「發送」按鈕提交一個問題到服務器端,服務器端實時確認收到查詢請求,5 秒後再將結果推送給客戶端。
測試工具方面有不少選擇,因爲許可證緣由,筆者採用了開源的 OWASP ZAP v2.4.3。這裏要簡單說一下,測試過程主要基於測試工具的代理,攔截到 WebSocket 握手請求以及 WebSocket 消息通訊,而後經過工具修改 Origin 後重發請求,若是鏈接成功後,重發 WebSocket 客戶端消息。以上功能各個商業安全測試工具均可以作到。
1. 首先在 Firefox 中配置好 ZAP 的代理,而後探索整個 WebSocket 應用。下圖能夠看到請求頭部有 HTTP Basic Authorization 信息,表示已經登陸成功。
2. 右鍵選擇重發 WebSocket 協議升級請求,將其中的 Origin 修改成任意其餘網址後點擊發送。
3. 點擊響應標籤,能夠看到服務器端返回了 101,即協議握手成功。
4. 進一步測試 WebSocket 消息是否能夠重發。以下圖所示,右鍵點擊第一條客戶端發出的 WebSocket 消息,選擇重發,輸入測試消息」www」後點擊發送,能夠看到 ZAP 陸續收到兩條服務器返回的消息。這充分證實被測試應用站點存在跨站點 WebSocket 劫持漏洞。
前文介紹了跨站點 WebSocket 劫持漏洞原理和檢測,相信讀者已經明白它的危害,接下來咱們談談如何防範這個漏洞。這個漏洞的原理聽起來略微複雜,但幸運的是測試起來相對比較簡單,那麼修復會不會也很簡單。不少讀者會想到,不就是在服務器代碼中檢查 Origin 參數嘛。是的,檢查 Origin 頗有必要,但不充分。筆者推薦你們要在服務器端的代碼中增長 Origin 檢查,若是客戶端發來的 Origin 信息來自不一樣域,建議服務器端拒絕這個請求,發回 403 錯誤響應拒絕鏈接。
筆者採用了 Java EE 技術編寫的 WebSocket 測試應用,Java EE 的 WebSocket API 中提供了配置器容許開發人員重寫配置用來攔截檢查協議握手過程。筆者在文章附錄的源代碼中已經包含了這部分代碼,下面簡單介紹一些核心類和配置。若是對 Java EE WebSocket API 不太熟悉的讀者,建議能夠先查閱相關規範。
1. 首先編寫一個 WebSocket 服務器終端的配置器,如清單 4 所示繼承並重寫 checkOrigin 方法。注意,筆者忽略了沒有 Origin 的場景,這一點要視各個應用的實際狀況而定,若是有非瀏覽器客戶端的話,則須要加上這一個檢查。同時建議非瀏覽器客戶端參見下文的令牌機制。
public class CustomConfigurator extends ServerEndpointConfig.Configurator { private static final String ORIGIN = "http://jeremy.laptop:8080"; @Override public boolean checkOrigin(String originHeaderValue) { if(originHeaderValue==null || originHeaderValue.trim().length()==0) return true; return ORIGIN.equals(originHeaderValue); } }
2. 而後將該配置器關聯到 WebSocket 服務器代碼中。
@ServerEndpoint(value = "/query", configurator = CustomConfigurator.class) public class WebSocketTestServer { @OnMessage public void onMessage(String message, Session session) throws IOException, InterruptedException { session.getBasicRemote().sendText("We got your query: " + message + "\nPlease wait for a while, we will response to you later."); Thread.sleep(5000); session.getBasicRemote().sendText("Sorry, we did not find the answer."); } }
3. 從新打包發佈 WebSocket 應用程序。
有興趣的讀者能夠本身嘗試,若是補上以上代碼後,重播篡改的 WebSocket 握手協議請求會收到 403 錯誤。
以上看起來很美好,可是僅僅檢查 Origin 遠遠不夠,別忘記了,若是 WebSocket 的客戶端不是瀏覽器,非瀏覽器的客戶端發來的請求根本就沒有 Origin。除此以外,咱們要記得,惡意網頁是能夠僞造 Origin 頭信息的。更完全的解決方案仍是要借鑑 CSRF 的解決方案-令牌機制。
鑑於篇幅緣由,筆者就不詳細貼出整個設計和代碼,建議讀者參照如下概要設計提升 WebSocket 應用的安全。
1. 服務器端爲每一個 WebSocket 客戶端生成惟一的一次性 Token;
2. 客戶端將 Token 做爲 WebSocket 鏈接 URL 的參數(譬如 ws://echo.websocket.org/?token=randomOneTimeToken),發送到服務器端進行 WebSocket 握手鍊接;
3. 服務器端驗證 Token 是否正確,一旦正確則將這個 Token 標示爲廢棄再也不重用,同時確認 WebSocket 握手鍊接成功;若是 Token 驗證失敗或者身份認證失敗,則返回 403 錯誤。
這個方案裏的 Token 設計是關鍵,筆者推薦的方案是爲登陸用戶生成一個 Secure Random 存儲在 Session 中,而後利用對稱加密(譬如 AES GCM)加密這個 Secure Random 值做爲令牌,將加密後的令牌發送給客戶端用來進行鏈接。這樣每一個 Session 有一個惟一的隨機數,每一個隨機數能夠經過對稱加密生成若干份一次性令牌。用戶即使經過不一樣終端經過 WebSocket 鏈接到服務器,服務器能夠在保障令牌惟一且一次性使用的前提下,依然能將不一樣通道中的信息關聯到同一用戶中。
可能存在另一個設計思路,在 WebSocket 消息中增長令牌和身份信息,但筆者以爲這樣的設計有悖於 WebSocket 的設計思想,並且增長了沒必要要的網絡負載。拋磚引玉,歡迎讀者提供更好的設計方案。
本文筆者跟讀者分享了對 WebSocket 協議握手的理解,並在此基礎上闡述了跨站點 WebSocket 劫持漏洞的原理。正如文中所提,已知的各種 WebSocket 漏洞中,只有這個是普遍存在於 Web 應用代碼中的漏洞。筆者同時分享了檢測跨站點 WebSocket 劫持漏洞的方法,而且基於 Java EE 技術介紹了漏洞的修復辦法,以及更全面的基於令牌機制的安全解決方案。
示例代碼 sourcecode.zip (9k)
原做者: 何健, 軟件架構師, 甲骨文(中國)軟件系統有限公司 2016 年 5 月 10 日
原文連接: https://www.ibm.com/developerworks/cn/java/j-lo-websocket-cross-site/index.html
原文連接:https://blog.csdn.net/cuixiping/article/details/70048611