於安全考量,從1995年Netscape開始,瀏覽器都實現同源策略(Same-origin policy),但是,隨著前端應用的多樣化,跨域請求的需求無可迴避,於是,後來CORS(Cross-Origin Resource Sharing)成了規範。

然而,看似簡單的協定中,藏了不少的細節。

同源策略與JSONP

同源策略中的「同源」指的是,瀏覽器中某些行為,必須與來源頁面的「協定」、「網域」、「連接埠」都相同,才能進行。

最初Netscape是限制不同頁面間Cookie的讀取,隨著瀏覽器功能越來越強,Ajax請求、Fetch請求等也在實作時,加上了限制,然而實際應用上,從多個網站取得資源是必須的,例如,OAuth 2中Implicit核發流程,就必須在允許跨域請求的條件下才能進行。

早期的XMLHttpRequest,只能請求與目前文件同源的其他頁面或資源,不同源的請求不會發出,為了繞過這個限制,開發者利用script標籤可透過src,下載非同源.js檔案的特性,動態建立script標籤,並附加至DOM樹,令瀏覽器下載非同源的指令稿,例如,伺服端可接受id=123&callback=handler請求參數,並傳回handler({"name":"Justin","age":35}),這是個函式呼叫,瀏覽器收到回應後就會執行。

若前端定義了callback指定的handler函式,執行時,就會收到{"name":"Justin","age":35},前端想跨域請求的資源,是附加在函式呼叫之中,而這就是JSONP(JSON with Padding)的原理;以jQuery為例,$.ajax函式可以發出非同步請求,當dataType設為'jsonp'時,使用的就不是XmlHttpRequest,而是JSONP實現,基於$.ajax的$.getJSON也是如此。

瞭解JSONP原理之後,自然就會知道,其限制為只能用GET請求,另一方面,JSONP不是正式定義的機制,而是繞過瀏覽器同源策略來發出請求的技法,此時,同源策略試圖避免的安全問題,就容易因為伺服端設計上的疏忽而發生。想想看,若沒有過濾惡意字元,callback請求參數故意使用了<body onload="alert('orz');"/><!--,結果將會如何呢?

跨域資源共享

在XMLHttpRequest Level 1規範之後,XMLHttpRequest本身就可以進行跨域請求了,Fetch API預設也支援跨域請求,它們實現了W3C正式的CORS規範(https://bit.ly/2UZWB5g),其中,不單只是規範瀏覽器處理跨域請求的方式,也規範了伺服端可控制的項目,像是允許的來源、請求方法、可否發送Cookie、可取得的回應標頭,甚至回應有效期限等。

支援CORS的瀏覽器,會在跨域請求時,自動處理細節,然而依舊必須知道,CORS將跨域請求分為:簡單(Simple),以及帶預檢(with Preflight)等兩種。在簡單請求中,方法只能是HEAD、GET、POST,而且,可用標頭,只能是Accept、Accept-Language、Content-Language、Last-Event-ID,以及Content-Type;而Content-Type也只允許三個值application/x-www-form-urlencoded、multipart/form-data、text/plain,此外都算入帶預檢的請求。

而且,簡單請求之所以「簡單」,是因為瀏覽器會直接發出跨域請求,並帶上Origin標頭表明來源,供伺服端用以判斷是否允許請求,若允許的話,可透過Access-Control-開頭的標頭來控制回應,最常見的就是Access-Control-Allow-Origin(後文簡稱ACAO),若設定為與Origin相同或者是「*」(不限制來源),瀏覽器就能允許指令稿拿到回應,否則就拋出錯誤,伺服端還可以使用Access-Control-Allow-Credentials,來表示瀏覽器是否可發送Cookie、Access-Control-Expose-Headers,決定瀏覽器可拿到的回應標頭。

若是另一種「帶預檢」的請求,瀏覽器會先使用OPTIONS來試探伺服器,並附上Origin及Access-Control-Request-Method、Access-Control-Request-Headers標頭,表明使用的請求方法,以及自訂的標頭,伺服端若允許請求,除了ACAO之外,還會附上Access-Control-Allow-Methods、Access-Control-Allow-Headers標頭,表示允許的方法及自訂標頭,之後,瀏覽器可依此決定伺服端是否同意跨域請求──若否,就拋出錯誤;若是,接下來就是正式進行跨域請求,方式就與簡單請求相同了。

安全性考量

基本上,同源策略是為了瀏覽器安全性,JSONP則是直接繞過了瀏覽器,除非是為了相容於老舊的瀏覽器,否則不建議使用。然而,CORS雖是正式規範,瀏覽器也會自動處理細節,但並非保證安全無虞。

由於CORS規範中,ACAO只允許設定為「*」或者是單一網域來源,若伺服端想要支援多個來源,就必須檢查Origin內容,決定是否允許並動態產生ACAO的值,Origin檢查規則若設計不良,例如,單純檢查前置或後置,就很容易被繞過,或者未過濾惡意符號,有機會造成某些組合可繞過Origin的檢查規則。

單就繞過Origin檢查規則來說,就可能達成攻擊內網的條件。舉例來說,若Access-Control-Allow-Credentials設定為true,攻擊者就可能藉由發送Cookie(例如將XMLHttpRequest的withCredentials設為true),進一步跨域取得使用者機密資料,或者執行CSRF攻擊;ACAO設定為「*」時,不會允許發送Cookie,然而,在動態生成其他網域值的情況下,非必要的話,就別將Access-Control-Allow-Credentials設為true。

另外,使用CORS須留意一下快取的問題。因為,瀏覽器可能對同樣URL的資源進行快取,而快取內容會包含回應標頭,在跨域時,就可能發生ACAO設定不同,然而,實際使用的是相同URL資源,結果卻直接從快取中取得,因而造成請求失敗等問題。類似的狀況,在〈條件式CORS回應下因缺少Vary: Origin導致的快取錯亂〉(https://bit.ly/2SHd1mp)、〈原來 CORS 沒有我想像中的簡單〉(https://bit.ly/2Neg1AP)都談到。

進一步地,惡意攻擊者也可能利用快取,特意在跨域請求時,將惡意程式碼儲存在快取,並在後續使用者請求的URL符合時,觸發程式碼的執行。

在Fetch規範的〈CORS protocol and HTTP caches〉(https://bit.ly/2GPuuC0)開頭就提到,「如果ACAO不是寫死或設為*,就要使用Vary」。例如,設定Vary: Origin標頭,對於相同URL的資源,然而Origin標頭不同的回應,瀏覽器可使用不同的快取,從而避免請求失敗或者可能的攻擊。

前、後端都應深入認識

對於CORS,其實有許多人停留在「CORS?伺服端加個ACAO不就好了」的階段,甚至特意用些簡單的方式,避開CORS實作上的麻煩。但是,別忘了!CORS與同源策略的安全性考量是相對的,如果真的要做跨域請求,詳加認識就絕對必要。

規格書也許讀來枯燥,然而MDN上的〈CORS〉(https://mzl.la/2lJKpWW)至少要好好讀過一遍,知道有哪些可控制的標頭,若要進一步了解更多資訊,〈The Complete Guide toCORS (In)Security〉(https://bit.ly/2Xd3CBp)是份不錯的指南,當中在簡略談了CORS之後,就討論了可能的攻擊模式,並提供防禦建議,因此,這不單是前端,後端也應該好好的瞭解的功課。

專欄作者

熱門新聞

Advertisement