同源策略和跨域請求研究


一、同源策略

假設有一個需求,需要向另外的網站請求數據,例如抓取谷歌搜索的結果。然后寫這么一個請求,搜索內容為hello:

var url = "https://www.google.com.hk/?gws_rd=cr,ssl#newwindow=1&safe=strict&q=hello";
$.ajax({
    url: url, 
    sucess: function(data){
        document.write(data);
    }
});

或者用原生的更直觀:

        var req = new XMLHttpRequest();
        req.open("GET", url);
        req.send();

執行后,瀏覽器會報錯:

大意是說localhost域名無法向google.com域名請求數據。

因為同源策略的限制,不同域名、協議(http、https)或者端口無法直接進行ajax請求。 同源策略只針對於瀏覽器端,瀏覽器一旦檢測到請求的結果的域名不一致后,會堵塞請求結果。這里注意,跨域請求是可以發去的,但是請求響應response被瀏覽器堵塞了

寫了一個程序做驗證——用node開了個服務,監聽在9000端口,然后在8000端口打開一個頁面,再向9000端口的服務發請求:

        url = "http://server.com:9000";
        $.ajax({
            method: "POST",
            url: url,
            data: {
                account: "yin"
            },
            success: function(data){
                document.write(data);
            }
        });

服務將收到的請求數據打印出來:

服務收到了請求,並正常返回數據,但是返回的數據被瀏覽器干掉了,即使是返回碼也無法得到了。所以說同源策略是限制了不同源的讀,但不限制不同源的寫。那么我們的問題來了,為什么不直接限制寫呢,只限制讀有什么好處呢?在回答這個問題之前,先要了解同源策略的作用。

假設我打開了A網銀http://Abank.com,已經通過了登陸驗證,然后再打開了另外一個黑網站http://evil.com,這個網站剛好是抓使用Abank.com的肉雞。在evil.com的代碼里會向Abank.com發請求,例如轉賬請求,將余額轉到自己的賬戶。但是由於同源策略的限制,使得這種做法無法成功。這個怎么解釋呢?

因為evil.com無法獲取你在Abank.com的信息,包括驗證身份的信息——通常是按照一定規則生成的無法猜到的隨機token字符串。token可能放在cookie里面,從evil.com向Abank發請求時,是不會帶上Abank的cookie的,同時也不會帶上evil.com的cookie,雖然cookie是和域名綁定的。由於沒有正確的token值,導致無法通過服務的身份驗證。

為驗證沒帶cookie,在上面的例子,localhost向server.com請求數據,服務將收到的cookie打印出來是undefined:

然而localhost已經設置了cookie:

server.com也有設置cookie:

回到上面的問題,為什么不限制寫呢?那是因為如果連請求也不出去,那在源頭上就限制死了,網站之間就無法共享資源了。另外,限制讀即瀏覽器攔截請求結果,一般情況下就夠了,一方面如果訪問的是黑網站,那么網站無法跟據請求結果繼續下一步的操作,如不斷地猜測密碼,另一方面如果訪問的是白網站,block掉請求結果,應該是考慮到了請求結果可能會使得頁面重定向,或者是給網頁添加一個惡意的iframe之類的。

有什么辦法可以繞過同源策略?有一個辦法就是CSRF攻擊

二、CSRF攻擊

如上面的例子,由於同源策略的限制,跨域的ajax請求不會帶cookie,然而script/iframe/img等標簽卻是支持跨域的,所以在請求的時候是會帶上cookie的。還是上面的例子,如果登陸了Abank.com,那么cookie里面就有了tocken,同時又打開了另外一個標簽頁訪問了evil.com,這個網頁里面有一個iframe:

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

這個iframe的src是一個Abank.com的轉賬的請求,如果Abank.com的轉賬請求沒有第二重加密措施的話,那么請求轉賬就成功了!

第二個例子是路由器的配置,假設我在網上找到了一個路由器配置教程的網站。這個網站里面偷偷地加一個img標簽:

<img src=”http://192.168.1.1/admin/config/outsideInterface?nexthop=123.45.67.89” alt=”pwned” height=”1” width=”1”/>

其中192.168.1.1是很多路由器的配置地址。這個1像素的圖片沒加載出來被忽略了,但是它的請求卻發出去了。這個請求給路由器添加了一個vpn代理,指向黑客的代理服務器。如果路由器也是把登陸驗證放在cookie里面,那么這個設置vpn的請求很可能就成功了,以后的連接路由器的每個請求都會先經過黑客的服務。

到這里,很明顯一個防CSRF攻擊的策略就是將token添加到請求的參數里面,也就是說每個需要驗證身份的請求都要顯式地帶上token值。詳見:Cross-Site Request Forgery Guide: Learn All About CSRF Attacks and CSRF Protection

 

用script引用的外域的資源一方面可以像上面一樣當作一個跨域的請求,另外一方面雖然資源是不可見的,但是script里面定義的全局對象是可用的,如引用jQuery的CDN,定義的一個全局對象jQuery。所以根據這個特性,在某些條件下可以獲得到script返回的需要登陸才能得到的數據,有興趣的可參見:Plain text considered harmful: A cross-domain exploit

跨域攻擊可以采取一些措施進行規避,但是跨域更多的還是一些實際的正常應用。

三、跨域請求

有時候在自己的網站需要一些去別人的網站請求數據,這個時候就需要跨域正常請求。方法有很多:

1. 跨域資源共享(CORS)

很多天氣、IP地址查詢的網站就采用了這樣的方法,允許其它網站對其請求數據,例如IP location,可以在自己網站的js里面向它發一個get請求:

        var url = "https://ipinfo.io/54.169.237.109/json?token=iplocation.net";
        document.cookie = "version=1;";
        $.ajax({ url: url })

它就會返回ip地址信息,同時不會被瀏覽器攔截:

觀察response的頭部,可以發現添加了一個字段:

Access-Control-Allow-Origin就是所謂的資源共享了,它的值*表示允許任意網站向這個接口請求數據,也可以設置成指定的域名,如:

response.writeHead(200, { "Access-Control-Allow-Origin": "http://yoursite.com"});

在node.js服務里面添加這個頭,那么只有http://yoursite.com能夠正常的進行跨域請求。更多地,還可以指定請求的方式、時間等,詳見:HTTP訪問控制(CORS)

2. JSONP

另外一個常用的辦法是使用jsonp,這個方法的原理是客戶端告訴服務一個回調函數的名稱,服務在返回的scritp里面調用這個回調函數,同時傳進客戶端需要的數據,這樣返回的代碼就在瀏覽器執行了。

例如8000端口要向9000端品請求數據,在8000端口的頁面文件定義一個回調函數writeDate,將writeDate寫在script的src的參數里,這個script標簽向9000端口發出請求:

    <script>
        function writeDate(_date){
            document.write(_date);
        }
    </script>
    <script src="http://192.168.0.103:9000/getDate?callback=writeDate"></script>

服務端返回一個腳本,在這個腳本里面執行writeDate函數:

function getDate(response, callback){
    response.writeHead(200, {"Content-Type": "text/javascript"});
    var data = "2016-2-19";
    response.end(callback + "('" + data + "')");
}

瀏覽器就執行了這個script片段:

這樣就實現了跨域的效果。jQuery的ajax里的jsonp的類型,就是用了這樣的辦法,只是jQuery將它封裝好了,使用起來形式跟普通的get/post一樣,但是原理是不一樣的。

JSONP和CORS相比較,缺點是只支持get類型,無法支持post等其它類型,必須完全信任提供服務的第三方,優點是兼容性較好。

3. 子域跨父域

子域跨父域是支持的,但是需要顯式將子域的域名改成父域的,例如mail.mysite.com要請求mysite.com的數據,那么在mail.mysite.com腳本里需要執行:

document.domain = "mysite.com";

4. iframe跨父窗口

如果iframe與父窗口也有同源策略的限制,父域無法直接讀取不同源的iframe的DOM內容以及監聽事件,但是iframe可以調用父窗口提供的api。iframe通過window.parent得到父窗口的window對象,然后父窗口定義一個全局對象供iframe調用。

例如在頁面通過iframe的方式嵌入一個youtobe的視頻,如果需要手動播放視頻、監聽iframe的播放事件,頁面需要引入youtobe的視頻播放控制api,在這個js文件里面定義了一個全局對象YT:

if (!window['YT']) {var YT = {loading: 0,loaded: 0};}

而在視頻iframe的腳本里通過window.parent獲取得到父窗口即自己網站的頁面:

sr = new Cq(window.parent, d, b)

自已網站的頁面也是在這個YT對象自定義一些東西,如添加播放事件監聽:

new YT.Player('video', { events:{ 'onStateChange': function(data){//do sth. } } });

5. window.postMessage

在上面第(4)點,父窗口無法向不同源的iframe傳遞東西,通過window.postMessage可以做到,父窗口向iframe傳遞一個消息,而iframe監聽消息事件。

例如在8000端口的頁面嵌入了一個9000端口的iframe:

<iframe src="http://server.com:9000"></iframe>

然后9000端口post一個message:

window.onload = function(){
            window.frames[0].postMessage("hello, this is from http://localhost:8000/", "http://server.com:9000/");
        }

postMessage執行的上下文必須是接收信息的window,傳遞兩個參數,第一個是數據,第二個是目標窗口。

同時,iframe即9000端口的頁面監聽message事件:

    window.addEventListener("message", receiveMessage);

    function receiveMessage(event){
        var origin = event.origin || event.originalEvent.origin; 
        //身份驗證
        if (origin !== "http://localhost:8000"){
              return;
        }
        console.log("receiveMessage: " + event.data); 
    }

這樣子iframe就可收到父窗口的信息了:

同理iframe也可以向父窗口發送消息:

window.parent.postMessage("hello, this is from http://server.com:9000", "http://localhost:8000");

父窗口收到:

window.postMessage也適用於通過window.open打開的子窗口,方法類似。

補充一點,如果iframe與父窗口是同源的,則父窗口可以直接獲取到iframe的內容,這個方法常用於無刷新上傳文件

 個人博客: http://yincheng.site/cross-domain


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM