比較深入全面地介紹了什麼是同源策略,同源策略的影響以及如何繞過同源策略進行CSRF攻擊。同時跨域請求的實際應用,通過案例分析跨域請求的幾種辦法:cors、jsonp、子域域父域、iframe跨父域、postMessage實現frame和父視窗的通信
一、同源策略
假設有一個需求,需要向另外的網站請求數據,例如抓取谷歌搜索的結果。然後寫這麼一個請求,搜索內容為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的內容,這個方法常用於無刷新上傳文件。