1. 程式人生 > >同源策略和跨域訪問

同源策略和跨域訪問

1. 什麼是同源策略 

    理解跨域首先必須要了解同源策略。同源策略是瀏覽器上為安全性考慮實施的非常重要的安全策略。
    何謂同源:
        URL由協議、域名、埠和路徑組成,如果兩個URL的協議、域名和埠相同,則表示他們同源。
    同源策略:
        瀏覽器的同源策略,限制了來自不同源的"document"或指令碼,對當前"document"讀取或設定某些屬性。 (白帽子講web安全[1])
        從一個域上載入的指令碼不允許訪問另外一個域的文件屬性。

    舉個例子:
        比如一個惡意網站的頁面通過iframe嵌入了銀行的登入頁面(二者不同源),如果沒有同源限制,惡意網頁上的javascript指令碼就可以在使用者登入銀行的時候獲取使用者名稱和密碼。


    在瀏覽器中,<script>、<img>、<iframe>、<link>等標籤都可以載入跨域資源,而不受同源限制,但瀏覽器限制了JavaScript的許可權使其不能讀、寫載入的內容。
    另外同源策略只對網頁的HTML文件做了限制,對載入的其他靜態資源如javascript、css、圖片等仍然認為屬於同源。

    程式碼示例(http://localhost:8080/和http://localhost:8081由於埠不同而不同源):
  1. http://localhost:8080/test.html  
  2.         <html>
  3.             <
    head><title>test same origin policy</title></head>
  4.             <body>
  5.                 <iframeid="test"src="http://localhost:8081/test2.html"></iframe>
  6.                 <scripttype="text/javascript">
  7.                     document.getElementById("test").contentDocument.body.innerHTML
     = "write somthing";  
  8.                 </script>
  9.             </body>
  10.         </html>
  11. http://localhost:8081/test2.html  
  12.         <html>
  13.             <head><title>test same origin policy</title></head>
  14.             <body>
  15.                 Testing.  
  16.             </body>
  17.         </html>

    在Firefox中會得到如下錯誤:
        Error: Permission denied to access property 'body'

    Document物件的domain屬性存放著裝載文件的伺服器的主機名,可以設定它。
    例如來自"blog.csdn.net"和來自"bbs.csdn.net"的頁面,都將document.domain設定為"csdn.net",則來自兩個子域名的指令碼即可相互訪問。
    出於安全的考慮,不能設定為其他主domain,比如http://www.csdn.net/不能設定為sina.com

2. Ajax跨域

    Ajax (XMLHttpRequest)請求受到同源策略的限制。
    Ajax通過XMLHttpRequest能夠與遠端的伺服器進行資訊互動,另外XMLHttpRequest是一個純粹的Javascript物件,這樣的互動過程,是在後臺進行的,使用者不易察覺。
    因此,XMLHTTP實際上已經突破了原有的Javascript的安全限制。
    舉個例子:
        假設某網站引用了其它站點的javascript,這個站點被compromise並在javascript中加入獲取使用者輸入並通過ajax提交給其他站點,這樣就可以源源不斷收集資訊。
        或者某網站因為存在漏洞導致XSS注入了javascript指令碼,這個指令碼就可以通過ajax獲取使用者資訊並通過ajax提交給其他站點,這樣就可以源源不斷收集資訊。
   如果我們又想利用XMLHTTP的無重新整理非同步互動能力,又不願意公然突破Javascript的安全策略,可以選擇的方案就是給XMLHTTP加上嚴格的同源限制。
   這樣的安全策略,很類似於Applet的安全策略。IFrame的限制還僅僅是不能訪問跨域HTMLDOM中的資料,而XMLHTTP則根本上限制了跨域請求的提交。(實際上下面提到了CORS已經放寬了限制)

   隨著Ajax技術和網路服務的發展,對跨域的要求也越來越強烈。下面介紹Ajax的跨域技術。

2.1 JSONP

    JSONP技術實際和Ajax沒有關係。我們知道<script>標籤可以載入跨域的javascript指令碼,並且被載入的指令碼和當前文件屬於同一個域。因此在文件中可以呼叫/訪問指令碼中的資料和函式。如果javascript指令碼中的資料是動態生成的,那麼只要在文件中動態建立<script>標籤就可以實現和服務端的資料互動。
    JSONP就是利用<script>標籤的跨域能力實現跨域資料的訪問,請求動態生成的JavaScript指令碼同時帶一個callback函式名作為引數。其中callback函式本地文件的JavaScript函式,伺服器端動態生成的指令碼會產生資料,並在程式碼中以產生的資料為引數呼叫callback函式。當這段指令碼載入到本地文件時,callback函式就被呼叫。

    第一個站點的測試頁面(http://localhost:8080/test.html):
  1. <scriptsrc="http://localhost:8081/test_data.js">
  2.     <script>
  3.         function test_handler(data) {  
  4.             console.log(data);  
  5.         }  
  6. </script>

    伺服器端的Javascript指令碼(http://localhost:8081/test_data.js):
        test_handler('{"data": "something"}');

    為了動態實現JSONP請求,可以使用Javascript動態插入<script>標籤:
  1. <scripttype="text/javascript">
  2.         // this shows dynamic script insertion  
  3.         var script = document.createElement('script');  
  4.         script.setAttribute('src', url);  
  5.         // load the script  
  6.         document.getElementsByTagName('head')[0].appendChild(script);   
  7. </script>

    JSONP協議封裝了上述步驟,jQuery中統一是現在AJAX中(其中data type為JSONP):
    http://localhost:8080/test?callback=test_handler

    為了支援JSONP協議,伺服器端必須提供特別的支援[2],另外JSONP只支援GET請求。

2.2 Proxy 

    使用代理方式跨域更加直接,因為SOP的限制是瀏覽器實現的。如果請求不是從瀏覽器發起的,就不存在跨域問題了。
    使用本方法跨域步驟如下:
    1. 把訪問其它域的請求替換為本域的請求
    2. 本域的請求是伺服器端的動態指令碼負責轉發實際的請求
    各種伺服器的Reverse Proxy功能都可以非常方便的實現請求的轉發,如Apache httpd + mod_proxy。
    Eg.
    為了通過Ajax從http://localhost:8080訪問http://localhost:8081/api,可以將請求發往http://localhost:8080/api。
    然後利用Apache Web伺服器的Reverse Proxy功能做如下配置:
        ProxyPass /api http://localhost:8081/api

2.3 CORS

2.3.1 Cross origin resource sharing

       “Cross-origin resource sharing (CORS) is a mechanism that allows a web page to make XMLHttpRequests to another domain. Such "cross-domain" requests would otherwise be forbidden by web browsers, per the same origin security policy. CORS defines a way in which the browser and the server can interact to determine whether or not to allow the cross-origin request. It is more powerful than only allowing same-origin requests, but it is more secure than simply allowing all such cross-origin requests.” ----Wikipedia[3]

   通過在HTTP Header中加入擴充套件欄位,伺服器在相應網頁頭部加入欄位表示允許訪問的domain和HTTP method,客戶端檢查自己的域是否在允許列表中,決定是否處理響應。
   實現的基礎是JavaScript不能夠操作HTTP Header。某些瀏覽器外掛實際上是具有這個能力的。

   伺服器端在HTTP的響應頭中加入(頁面層次的控制模式):
   Access-Control-Allow-Origin: example.com
   Access-Control-Request-Method: GET, POST
   Access-Control-Allow-Headers: Content-Type, Authorization, Accept, Range, Origin
   Access-Control-Expose-Headers: Content-Range
   Access-Control-Max-Age: 3600
   多個域名之間用逗號分隔,表示對所示域名提供跨域訪問許可權。"*"表示允許所有域名的跨域訪問。

   客戶端可以有兩種行為:
   1. 傳送OPTIONS請求,請求Access-Control資訊。如果自己的域名在允許的訪問列表中,則傳送真正的請求,否則放棄請求傳送。
   2. 直接傳送請求,然後檢查response的Access-Control資訊,如果自己的域名在允許的訪問列表中,則讀取response body,否則放棄。
   本質上服務端的response內容已經到達本地,JavaScript決定是否要去讀取。

   Support: [Javascript Web Applications]
   * IE >= 8 (需要安裝caveat)
   * Firefox >= 3
   * Safari 完全支援
   * Chrome 完全支援
   * Opera 不支援

 2.3.2 測試

   測試頁面http://localhost:8080/test3.html使用jquery傳送Ajax請求。
  1. <html>
  2.         <head><title>testing cross sop</title></head>
  3.         <body>
  4.             Testing.  
  5.             <scriptsrc="jquery-2.0.0.min.js"></script>
  6.             <scripttype='text/javascript'>
  7.                 $.ajax({  
  8.                     url: 'http://localhost:8000/hello',  
  9.                     success: function(data) {  
  10.                         alert(data);  
  11.                     },  
  12.                     error: function() {  
  13.                         alert('error');  
  14.                     }  
  15.                 });  
  16.             </script>
  17.         </body>
  18. </html>
    測試Restful API(http://localhost:8000/hello/{name})使用bottle.py來host。
    from bottle import route, run, response
    @route('/hello')
    def index():
        return 'Hello World.'
    run(host='localhost', port=8000)

    測試1:
        測試正常的跨域請求的行為。
    測試結果:
        1. 跨域GET請求已經發出,請求header中帶有
            Origin    http://localhost:8080
        2. 伺服器端正確給出response
        3. Javascript拒絕讀取資料,在firebug中發現reponse為空,並且觸發error回撥

    測試2:
        測試支援CORS的伺服器的跨域請求行為。
        對Restful API做如下改動,在response中加入header:
             def index():
                #Add CORS header#
                response.set_header("Access-Control-Allow-Origin", "http://localhost:8080")
                return 'Hello World.'
    測試結果:
        1. 跨域GET請求已經發出,請求header中帶有
            Origin    http://localhost:8080
        2. 伺服器端正確給出response
        3. 客戶端正常獲取資料

    測試3:
        測試OPTIONS請求獲取CORS資訊。
        對客戶端的Ajax請求增加header:
  1. $.ajax({  
  2.           url: 'http://localhost:8000/hello',  
  3.           headers: {'Content-Type': 'text/html'},  
  4.           success: function(data) {  
  5.               alert(data);  
  6.           },  
  7.           error: function() {  
  8.               alert('error');  
  9.           }  
  10.       });  


        對Restful API做如下改動:
            @route('/hello', method = ['OPTIONS', 'GET'])
            def index():
                if request.method == 'OPTIONS':
                    return ''
                return 'Hello World.'
    測試結果:
    1. Ajax函式會首先發送OPTIONS請求
    2. 針對OPTIONS請求伺服器
    3. 客戶端發現沒有CORS header後不會發送GET請求

    測試4:
        增加伺服器端對OPTIONS方法的處理。
        對Restful API做如下改動:
            @route('/hello', method = ['OPTIONS', 'GET'])
            def index():
                response.headers['Access-Control-Allow-Origin'] = 'http://localhost:8080'
                response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS'
                response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type'
                if request.method == 'OPTIONS':
                    return ''
                return 'Hello World.'
    測試結果:
        1. Ajax函式會首先發送OPTIONS請求
        2. 針對OPTIONS請求伺服器
        3. 客戶端匹配CORS header中的allow headers and orgin後會正確傳送GET請求並獲取結果
        測試發現,Access-Control-Allow-Headers是必須的。

   CORS協議提升了Ajax的跨域能力,但也增加了風險。一旦網站被注入指令碼或XSS攻擊,將非常方便的獲取使用者資訊並悄悄傳遞出去。

4. Cookie 同源策略

    Cookie中的同源只關注域名,忽略協議和埠。所以https://localhost:8080/和http://localhost:8081/的Cookie是共享的。

5. Flash/SilverLight跨域

    瀏覽器的各種外掛也存在跨域需求。通常是通過在伺服器配置crossdomain.xml[4],設定本服務允許哪些域名的跨域訪問。
    客戶端會首先請求此檔案,如果發現自己的域名在訪問列表裡,就發起真正的請求,否則不傳送請求。
        <?xml version="1.0"?>
            <!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
            <cross-domain-policy>
            <allow-access-from domain="*"/>
            <allow-http-request-headers-from domain="*" headers="*"/>
        </cross-domain-policy>
    通常crossdomain.xml放置在網站根目錄。

6. 總結 

    網際網路的發展催生了跨域訪問的需求,各種跨域方法和協議滿足了需求但也增加了各種風險。尤其是XSS和CSRF等攻擊的盛行也得益於此。
    瞭解這些技術背景有助於在實際專案中熟練應用並規避各種安全風險。

Reference

[1] 白帽子講Web安全: http://book.douban.com/subject/10546925/
[2] 使用 JSONP 實現跨域通訊: http://www.ibm.com/developerworks/cn/web/wa-aj-jsonp1/
[3] Cross-origin resource sharing: http://en.wikipedia.org/wiki/Cross-Origin_Resource_Sharing
[4] Cross-domain policy for Flash movies: http://kb2.adobe.com/cps/142/tn_14213.html