目錄
有關於瀏覽器的同源策略和如何跨域獲取資源,傳送門——> 瀏覽器同源策略和跨域的實現方法
同源策略(SOP)限制了應用程序之間的信息共享,並且僅允許在托管應用程序的域內共享。這有效防止了系統機密信息的泄露。但與此同時,也帶來了另外的問題。隨着Web應用程序和微服務使用的日益增長,出於實用目的往往需要將信息從一個子域傳遞到另一個子域,或者在不同域之間進行傳遞(例如將訪問令牌和會話標識符,傳遞給另一個應用程序)。
為了允許跨域通信,開發人員必須使用不同的技術來繞過SOP並傳遞敏感信息,以至於現今也成為了一個棘手的安全問題。因此,為了在不影響應用程序安全狀態的情況下實現信息共享,在HTML5中引入了跨源資源共享(CORS)。但問題也隨之而來,許多人為了方便干脆直接使用默認的配置,或是由於缺乏對此的了解而導致了錯誤的配置。
CORS跨域資源共享
跨域資源共享(CORS)是一種放寬同源策略的機制,它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制,以使不同的網站可以跨域獲取數據。
我們先來簡單分析一下CORS跨域獲取資源的過程:
CORS定義了兩種跨域請求:簡單請求 和 非簡單請求。簡單跨域請求就是使用設定的請求方式請求數據,而非簡單跨域請求則是在使用設定的請求方式請求數據之前,先發送一個OPTIONS預檢請求,驗證請求源是否為服務端允許源。只有"預檢"通過后才會再發送一次請求用於數據傳輸。
當我們需要發送一個跨域請求的時候,瀏覽器會首先檢查這個請求,如果它是簡單跨域請求,瀏覽器就會立刻發送這個請求。如果它是非簡單跨域請求,這時候瀏覽器不會馬上發送這個請求,而是有一個跟服務器預檢驗證的過程。
簡單跨域請求
(1) 請求方法是以下三種方法之一:
HEAD
GET
POST
(2)HTTP的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:application/x-www-form-urlencoded、 multipart/form-data、text/plain
只有同時滿足以上兩個條件時,才是簡單請求,否則為非簡單請求。
瀏覽器判斷該請求為簡單請求時,會在Request Header中添加 Origin 字段,它表示我們的請求源。
如下,簡單請求頭:
CORS服務端會將該字段作為跨源標志。CORS接收到此次請求后, 首先會判斷Origin是否在允許源(由服務端決定)范圍之內。
如果Origin指定的源在許可范圍內,即驗證通過,服務端會在Response Header 添加下面幾個字段
- Access-Control-Allow-Origin:該字段是必須的。它的值要么是請求時
Origin
字段的值,要么是一個*
,表示接受任意域名的請求。 - Access-Control-Allow-Credentials:該字段可選。它的值是一個布爾值,表示是否允許發送Cookie。默認情況下,Cookie不包括在CORS請求之中。當設置為
true時
,即表示服務器明確許可,Cookie可以包含在請求中,一起發給服務器。這個值也只能設為true
,如果服務器不要瀏覽器發送Cookie,刪除該字段即可 - Access-Control-Expose-Headers:該字段可選。CORS請求時,
XMLHttpRequest
對象的getResponseHeader()
方法只能拿到6個基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必須在Access-Control-Expose-Headers
里面指定。
如下,CROS服務端的回應:
如果Origin
指定的源不在許可范圍內,即驗證失敗,服務器也會返回一個正常的HTTP回應。瀏覽器發現,這個回應的頭信息中的Access-Control-Allow-Origin
字段不包含訪問源,就知道出錯了,從而拋出同源檢測異常的錯誤。注意,這種錯誤無法通過狀態碼識別,因為HTTP回應的狀態碼有可能是200。
上面說到,CORS請求默認不發送Cookie和HTTP認證信息。如果要把Cookie發到服務器,一方面要服務器同意,指定Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials:true
另一方面,開發者必須在AJAX請求中打開withCredentials
屬性
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
否則,即使服務器同意瀏覽器發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。
但是,如果省略withCredentials
設置,有的瀏覽器還是會一起發送Cookie。這時,可以顯式關閉withCredentials
xhr.withCredentials = false;
需要注意的是,如果要發送Cookie,即Access-Control-Allow-Credentials:true時,Access-Control-Allow-Origin
就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie才會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁代碼中的document.cookie
也無法讀取服務器域名下的Cookie。
總結:簡單請求只需要CORS服務端在接受到攜帶Origin字段的跨域請求后,在response header中添加Access-Control-Allow-Origin等字段給瀏覽器做同源判斷。
非簡單請求
非簡單請求是那種對服務器有特殊要求的請求,比如請求方法是PUT
或DELETE
,或者Content-Type
字段的類型是application/json
。非簡單請求的CORS請求,會在正式通信之前,增加一次OPTIONS方法的預檢請求。
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可以使用哪些HTTP動詞和頭信息字段。只有得到肯定答復,瀏覽器才會發出正式的XMLHttpRequest
請求,否則就報錯。
下面簡單分析一下非簡單跨域請求的過程。瀏覽器先發送一個OPTIONS方法的預檢請求。帶有如下字段:
- Origin: 在CORS中專門作為Origin信息供后端比對,表明來源域。
- Access-Control-Request-Method: 接下來請求的方法,例如PUT、DELETE等等
- Access-Control-Request-Headers: 自定義的頭部,所有用setRequestHeader方法設置的頭部都將會以逗號隔開的形式包含在這個頭中
然后如果服務器配置了CORS,會返回對應對的字段,具體字段含義在返回結果是一並解釋。
- Access-Control-Allow-Origin: 允許進行跨域請求的域名
- Access-Control-Allow-Methods: 允許進行跨域請求的方式
- Access-Control-Allow-Headers: 允許進行跨區請求的頭部
如下,OPTIONS預檢的請求與相應
然后瀏覽器再根據服務器的返回值判斷是否發送非簡單請求。然后服務器處理完請求之后,會再返回結果中加上如下控制字段:
- Access-Control-Allow-Origin: 允許跨域訪問的域,可以是一個域的列表,也可以是通配符"*"。這里要注意Origin規則只對域名有效,並不會對子目錄有效。即http://foo.example/subdir/ 是無效的。但是不同子域名需要分開設置,這里的規則可以參照同源策略
- Access-Control-Allow-Credentials: 是否允許請求帶有驗證信息
- Access-Control-Expose-Headers: 允許腳本訪問的返回頭,請求成功后,腳本可以在XMLHttpRequest中訪問這些頭的信息
- Access-Control-Max-Age: 緩存此次請求的秒數。在這個時間范圍內,所有同類型的請求都將不再發送預檢請求而是直接使用此次返回的頭作為判斷依據,非常有用,大幅優化請求次數
- Access-Control-Allow-Methods: 允許使用的請求方法,以逗號隔開
- Access-Control-Allow-Headers: 允許自定義的頭部,以逗號隔開,大小寫不敏感
然后瀏覽器通過返回結果的這些控制字段來決定是將結果開放給客戶端腳本讀取還是屏蔽掉。如果服務器沒有配置CORS,返回結果沒有控制字段,瀏覽器會屏蔽腳本對返回信息的讀取,並報出同源檢測異常的錯誤!
通過上面敘述,我們得知借助CORS我們不必關心發出的請求是否跨域,瀏覽器會幫我們處理這些事情,但是服務端需要支持CORS,服務端實現CORS的原理也很簡單,在服務端完全可以對請求做上下文處理,已達到接口允許跨域訪問的目的。
當然,也有很多第三方的CORS插件,例如:Spring MVC 在4.2以上版本也支持了CORS配置,這樣,服務端也無需自己操心了!
CORS的安全問題
CORS非常有用,可以共享許多內容,不過這里存在風險。因為它完全是一個盲目的協議,只是通過HTTP頭來控制的。那么,CORS跨域資源共享漏洞是怎么發生的呢?由於程序員配置不當,Origin源不嚴格,從而造成跨域問題。
由以上可知,網站可以通過發送以下HTTP響應頭部來啟用CORS:
Access-Control-Allow-Origin: https://example.com
這樣的話,就可以允許指定的源(http://example.com)來跨域請求服務器端的資源,並且服務器會響應。在默認情況下,發送跨域請求時不會攜帶cookie或其他憑據。因此,它不能用於竊取與用戶相關的敏感信息(如CSRF令牌)。不過,網站服務器可以使用以下頭部來啟用憑據傳輸:
Access-Control-Allow-Credentials:true
這樣瀏覽器在請求數據的時候就需要帶上cookie。
實現對單個域的信任是非常容易的事情。不過,如果需要信任多個域的話,那該怎么辦呢?根據相關規范的建議,只需列出相關的域,並用空格加以分隔即可,例如:
Access-Control-Allow-Origin:http://a.example.com http://example.com
但是,沒有哪個瀏覽器真正支持這一特性。
於是,我們可以通過使用通配符來信任所有子域,具體方法是:
Access-Control-Allow-Origin: *.example.com
可是有一些偷懶的程序員,將Access-Control-Allow-Origin設置為允許來自所有域*的跨域請求。
Access-Control-Allow-Origin:*
這樣,所有的網站都可以對其進行跨域資源請求了,這是非常危險的。不過先別高興的太早。其實這里在設計的時候有一個很好的限制。xmlhttprequest發送的請求需要使用 "withCredentials" 來帶上Cookie,如果一個目標域設置成了允許任意域的跨域請求,這個請求又帶着 Cookie 的話,這個請求是不合法的。(就是如果需要實現帶 Cookie 的跨域請求,CORS服務端需要明確的配置允許來源的域,使用任意域的配置是不合法的)瀏覽器會屏蔽掉返回的結果。Javascript 就沒法獲取返回的數據了。這是CORS模型最后一道防線。假如沒有這個限制的話,那么 Javascript 就可以獲取返回數據中的 Cookie 和 CSRF Token,以及各種敏感數據。這個限制極大的降低了CORS的風險。
如下,這是不允許的:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
這時,將在瀏覽器控制台中收到錯誤消息:當憑證標志為true時,無法在Access-Control-Allow-Origin中使用通配符(各個瀏覽器報錯顯示的不一樣)。
那么,CORS的漏洞到底出現在哪里呢?
1:CORS服務端的 Access-Control-Allow-Origin 設置為了 *,並且 Access-Control-Allow-Credentials 設置為false,這樣任何網站都可以獲取該服務端的任何數據了。
2:有一些網站的Access-Control-Allow-Origin他的設置並不是固定的,而是根據用戶跨域請求數據的Origin來定的。這時,不管Access-Control-Allow-Credentials 設置為了 true 還是 false。任何網站都可以發起請求,並讀取對這些請求的響應。意思就是任何一個網站都可以發送跨域請求來獲得CORS服務端上的數據。
下面的代碼是通過AJAX來跨域請求獲取服務端的數據
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ajax</title>
<script type="text/javascript">
function foo(){
var xmlhttp=new XMLHttpRequest();
var url="http://127.0.0.1/1.txt"; //要跨域訪問的資源
xmlhttp.open("POST",url,true);
//xmlhttp.setRequestHeader('X-PINGOTHER','AAAA'); //自定義頭部,如果這樣的話,就屬於非簡單請求了
//xmlhttp.setRequestHeader('Content-Type','text/xml'); //自定義頭部,如果這樣的話,就屬於非簡單請求了
xmlhttp.send();
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("my").innerHTML=xmlhttp.responseText;
}
}
}
</script>
</head>
<body>
<button id="btn" οnclick="foo()">確定</button>
<p id="my">hello,word!</p>
</body>
</html>
CORS漏洞的利用