我知道的跨域與安全

人人網FED發表於2018-01-20

關於跨域,有兩個誤區

1. ✕ 動態請求就會有跨域的問題

✔ 跨域只存在於瀏覽器端,不存在於安卓/ios/Node.js/python/ java等其它環境

2. ✕ 跨域就是請求發不出去了

✔ 跨域請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了

之所以會跨域,是因為受到了同源策略的限制,同源策略要求源相同才能正常進行通訊,即協議、域名、埠號都完全一致。

如下圖所示:

這三個源分別由於域名、協議和埠號不一致,導致會受到同源策略的限制。

同源策略具體限制些什麼呢?

1. 不能向工作在不同源的的服務請求資料(client to server)

這裡有個問題之前也困擾了我很久,就是為什麼home.com載入的cdn.home.com/index.js可以向home.com發請求而不會跨域呢?其實home.com載入的JS是工作在home.com的,它的源不是提供JS的cdn,所以這個時候是沒有跨域的問題的,並且script標籤能夠載入非同源的資源,不受同源策略的影響。

2. 無法獲取不同源的document/cookie等BOM和DOM,可以說任何有關另外一個源的資訊都無法得到 (client to client)


為什麼會有同源策略呢?

1. 為什麼要限制不同源發請求?

假設使用者登陸了bank.com,同時開啟了evil.com,如果沒有任何限制,evil.com可以向bank.com請求到任何資訊,進而就可以在evil.com向bank.com發轉賬請求等。

如果這樣,為什麼不直接限制寫,只限制讀?

因為如果連請求都發不出去了,那就不能做跨域資源共享了,無法讀取返回結果,evil.com就無法繼續下一步的操作,如獲取轉賬請求的一些必要的驗證資訊。

2. 為什麼限制跨域的DOM讀取?

如果不限制的話,那麼很容易就可以偽裝其它的網站,如套一個iframe或者通過window.open的方法,從而得到使用者的操作和輸入,如賬戶、密碼。

另外,新增這個http頭可以限制別人把你的網站套成它的iframe:

X-Frame-Options: SAMEORIGIN


同源策略提供了安全的同時也造成了不方便,因為有時候我們需要跨域請求,如獲取第三方提供的服務資訊,由於第三方的源和本網站的源不一樣,所以這個時候就受到跨域的限制。

跨域最常用的方法,應當屬CORS,如下圖所示:

只要瀏覽器檢測到響應頭帶上了CORS,並且允許的源包括了本網站,那麼就不會攔截請求響應。

CORS把請求分為兩種,一種是簡單請求,另一種是需要觸發預檢請求,這兩者是相對的,怎樣才算“不簡單”?只要屬於下面的其中一種就不是簡單請求:

(1)使用了除GET/POST/HEAD之外的請求方式,如PUT/DELETE

(2)使用了除Content-Type/Accept等幾個常用的http頭

這個時候就認為需要先發個預檢請求,預檢請求使用OPTIONS方式去檢查當前請求是否安全,如下圖所示:

程式碼裡面只發了一個請求,但在控制檯看到了兩個請求,第一個是OPTIONS,服務端返回:

返回頭裡麵包含了允許的請求頭、請求方式、源,以及預檢請求的有效期,上圖是設定了20天,在這個有效期內就不用再發一個options的請求,實際上瀏覽器有一個最長時間,如Chrome是5分鐘。如果在預檢請求檢測到當前請求不符合服務端設定的要求,則不會發出去了直接拋異常,這個時候就不用去發“複雜”的請求了。

如本源不在允許的源範圍內,則會拋異常,無法獲取返回結果:

為了支援CORS,nginx可以這麼配:

location / {
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
     }
     add_header 'Access-Control-Allow-Origin' '*';
     add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
     add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
     add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
}複製程式碼


第二種常用的跨域的方法是JSONP,JSONP是利用了script標籤能夠跨域,如下程式碼所示:

function updateList (data) {
    console.log(data);
}

$body.append(‘<script src=“http://otherdomain.com/request?callback=updateList"></script>');複製程式碼

程式碼先定義一個全域性函式,然後把這個函式名通過callback引數新增到script標籤的src,script的src就是需要跨域的請求,然後這個請求返回可執行的JS文字:

// script響應返回的js內容為
updateList([{
    name: 'hello'
}]);複製程式碼

由於它是一個js,並且已經定義了upldateList函式,所以能正常執行,並且跨域的資料通過傳參得到。這就是JSONP的原理。


所以由於script/iframe/img等標籤的請求預設是能帶上cookie(cookie裡面帶上了登陸驗證的票token),用這些標籤發請求是能夠繞過同源策略的,因此就可以利用這些標籤做跨站請求偽造(CSRF),如下面程式碼所示:

// 轉賬請求
<iframe src="http://Abank.com/app/transferFunds?amount=1500&destinationAccount=..."></iframe>

// 配置路由器新增代理
<img src="http://192.168.1.1/admin/config/outsideInterface?nexthop=123.45.67.89" style="display:none">複製程式碼

如果相應的網站支援GET請求,或者沒有做進一步的防護措施,那麼如果使用者在另外一個頁面登陸過了,再開啟一個“有毒”的網站就中招了。

而動態ajax請求預設是不帶cookie的,如果你要帶cookie,可以設定ajax的一個屬性withCredentials,如下程式碼所示:

// 原生請求
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("GET", "http://otherdomain.com/list");
xhr.send();

// jquery請求
$.ajax({
   url: "http://otherdomain.com/list",
   xhrFields: {
      withCredentials: true
   }
});

複製程式碼

這個時候就和img/script標籤一樣,能帶上cookie,並且還支援除GET之外的其它方式。所以這種方式也是能實現CSRF的,如下圖所示:

所以如果轉賬請求只是不支援GET,沒做其它的防護措施,仍然有CSRF攻擊的風險。那怎麼辦呢?

方法一是每次請求都要在引數裡面顯示地帶上token即登陸的票,雖然跨域請求能帶上cookie,但是通過document.cookie仍然是獲取不到其它源的cookie的,所以攻擊者無法在程式碼裡面拿到cookie裡面的token,所以就沒辦法了。方法一的缺點是會暴露token,所以需要帶token的最好不能是GET,因為GET會把引數拼在url裡面,使用者可能會無意把連結發給別人,但不知道這個連結帶上了自己的登陸資訊。

方法二是每次轉賬請求前都先請求一個隨機串,這個串只能用一次轉賬或者支付請求,用完就廢棄,只有這個串對得上才能請求成功,攻擊者是無法拿到這個串的,因為如果跨域請求帶cookie,瀏覽器要求Access-Control-Allow-Origin不能為萬用字元,只能為指定的源,如:

Access-Control-Allow-Origin: http://renren.com
由於攻擊者所在的域名不在這個源裡面,所以它是無法得到請求結果,所以請求不到隨機串。因此這種方式也是可以避免CSRF攻擊。
假設Allow-Origin為*,ajax設定withCredentials為true時,瀏覽器會拋異常,無法得到返回結果:
另外服務還需要指定Allow-Credentials的頭部,如下程式碼所示:
add_header "Access-Control-Allow-Origin" "http://fedren.com";
add_header "Access-Control-Allow-Credentials" "true";複製程式碼

關於cookie還有兩個地方值得注意,如下圖所示:


討論完了client to server,我們再討論client to client,即如何和一個frame通訊,包括iframe或者使用window.open開啟的頁面。

iframe訪問父頁面可通過window.parent得到父視窗的window物件,通過open開啟的可以用window.opener,進而得到父視窗的任何東西;父視窗如果和iframe同源的,那麼可通過iframe.contentWindow得到iframe的window物件,如果和iframe不同源,則存在跨域的問題,這個時候可通過postMessage進行通訊。

使用postMessage的基本原理如下圖所示:

// main frame
let iframeWin = document.querySelector("#my-iframe")
                .contentWindow;
iframeWin.postMessage({age: 18}, "http://parent.com");
iframeWin.onmessage = function(event) {
    console.log("recv from iframe ", event.data);
};

// iframe
window.onmessage = function(event) {
    // test event.origin
    if (event.origin !== expectOrigin) {
        return;
    }
    console.log("recv from main frame ", event.data);
};

window.parent.postMessage("hello, this is from iframe ", "http://child.com");
複製程式碼

以頁面嵌入youtobe視訊為例,通過以下程式碼可以在頁面嵌入一個youtobe視訊,嵌入的是一個跨域的iframe,所以就涉及到如何和iframe進行通訊的問題。如怎麼知道iframe的狀態,觸發父頁面定義的事件onPlayerReady,這個是iframe通知父頁面,而父頁面可以調player.stopVideo控制iframe的行為,這個是父頁面通知iframe。

iframe通知父頁面是通過window.parent.postMessage,同時監聽message事件:

經檢查上面程式碼4304行的c就是window.parent,這個embed-player.js是iframe的js,iframe的js通過postMessage傳送了一個訊息,如上圖右邊的視窗所示,然後在父視窗的widgetapi.js就收到了這個訊息。

同樣地,父視窗的JS也是使用postMessage向iframe傳送訊息,如下圖所示:

當然postMessage不限於跨域,同域的也可以使用,只是同域的話可以通過window物件互相操作,你可能需要額外定義一些全域性變數或者函式供其它frame使用,或者是定義一套事件機制(可以藉助原生事件/jQuery/Vue事件等)。

這裡有一個特例,就是子域如mail.hello.com要跨hello.com的時候,可以顯式地設定子域的document.domain值為父域的domain:

document.domain = "hello.com";複製程式碼
就不會有跨域的問題了。

補充一點,如果需要和同源的不同標籤頁進行通訊可以使用localStorage,即一個頁面設定localStorage,其它頁面就會觸發storage事件:
window.addEventListener('storage', function(e) {
    e.key;
    e.oldValue;
    e.newValue;
    e.url;
    e.storageArea;
});
複製程式碼

這個我沒試過,讀者可以試一下。

再補充一點,websocket是不受同源策略限制的,沒有跨域的問題。CSS的字型檔案是會有跨域問題,指定CORS就能載入其它源的字型檔案(通常是放在cdn上的)。而canvas動態載入的外部image,也是需要指定CORS頭才能進行圖片處理,否則只能畫不能讀取。


最後,跨域分為兩種,一種是跨域請求,另一種訪問跨域的頁面,跨域請求可以通過CORS/JSONP等方法進行訪問,跨域的頁面主要通過postMesssage的方式。由於跨域請求不但能發出去還能帶上cookie,所以要規避跨站請求偽造攻擊的風險,特別是涉及到錢的那種請求。


相關文章