跨域與同源策略探究

背景

跨域這個問題前端開發者都接觸過,網上的文章也非常多,但是昨天的騰訊二面給我留了非常深刻的印象,原來跨域能問出那么多花樣,難怪所有面試官都喜歡和面試者來探討這個問題。

跨域

一、什么是跨域

簡單的來說就是瀏覽器限制了向不同域發送ajax請求。

不同域體現在:域名、端口、協議不同

二、怎么解決跨域

1.JSONP

JSONP在CORS出現之前,是最常見的一種跨域方案,在IE10以下的版本,是不兼容CORS的,所以如果有需要兼容IE10一下的,都會使用JSONP去解決跨域問題。

JSONP的基本原理:

動態向頁面添加一個script標簽,瀏覽器對腳本請求是沒有域限制的,瀏覽器會請求腳本,然后解析腳本,執行腳本。通過這個我們就可以實現跨域請求。

function handleResponse(response) {
  console.log(response)
}
var script = document.createElement("script")
script.src = "http://localhost:3000/?callback=handleResponse"
document.body.insertBefore(script, document.body.firstChild)

研究一下這段代碼,新建一個script標簽,設置標簽的src屬性,動態插入標簽到body后面。插入以后瀏覽器會請求src的內容,下載下來并執行。

那怎么通過回調handleResponse獲得數據?src后面那段querystring又是干什么的?

如果請求得到的腳本里面的代碼長這樣

handleResponse('hello world')

執行的時候是不是就可以通過回調得到data -> ‘hello world’

所以jsonp其實也需要后端的支持,這個queryString就是讓后端知道你前端的回調方法,然后要返回怎樣的腳本給前端。

const koa = require('koa')
const app = new koa()
app.use(async ctx => {
  ctx.body = `${ctx.query.callback}('hello world')`;
});
app.listen(3000)

這是node的一段代碼,簡單的體現出了jsonp后端的處理方式。

JSONP的缺點

只能發送get請求,感覺這不是正經的手段,而是一個奇淫巧技。

對于,為什么瀏覽器對于JavaScript不做同源策略這個問題,我覺得主要原因是需要后端的支持,他需要通過后端返回的內容,并且執行,產生回調才可以取得到結果。然而ajax不一樣,js可以直接拿到ajax的返回結果。

還有一種可能就是,靜態資源需要放到CDN上,或者引用一些公共的腳本,應該是需求導致。

2.CORS

CORS是一個W3C標準,全稱是”跨域資源共享”(Cross-origin resource sharing)。

它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

什么是CORS?

CORS需要瀏覽器和服務器同時支持。目前,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10。

整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。對于開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。

因此,實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨源通信。

拿Koa舉個栗子。在Koa中,我們只需要在Koa中加個中間件

const koa = require('koa')
const cors = require('koa2-cors')
const app = new koa()
app.use(cors())
app.use(async ctx => {
  ctx.body = 'hello world';
});
app.listen(3000)

這樣就能實現服務端CORS接口。

兩種請求

關于為什么有這兩種請求的區別,我個人認為,瀏覽器發送預請求所消耗的資源會比簡單請求還多,所以瀏覽器發送簡單請求不需要發送預請求。而非簡單請求,如果發送以后,然后被服務器拒絕了,所消耗的資源比預請求還多,所以在發送非簡單請求之前,使用一個預請求來判斷服務器是否允許跨域。

1.簡單請求

同時滿足一下條件為簡單請求

(1) 請求方法是以下三種方法之一:

  • HEAD
  • GET
  • POST

(2)HTTP的頭信息不超出以下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三個值application/x-www-form-urlencodedmultipart/form-datatext/plain

如果content-type的值為application/json那么這個請求就是非簡單請求,需要發送預請求。

2.非簡單請求

不屬于簡單請求都是非簡單請求,非簡單請求就需要預請求。

預請求

非簡單請求會在正式請求之前發送一次預請求,這個請求是瀏覽器發的。瀏覽器先向服務器詢問,當前的請求域是否在服務器的許可名單中,已經服務器允許哪一些方法,哪一些請求頭。只有得到服務器的答復,并且之后發送的那個正式請求是被允許的(方法和請求頭),瀏覽器才會發送這個正式請求。

“預檢”請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息里面,關鍵字段是Origin,表示請求來自哪個源。

除了Origin字段,”預檢”請求的頭信息包括兩個特殊字段。

Access-Control-Request-Method

字面意思,內容是允許的請求方法

Access-Control-Request-Headers

同字面意思,內容是允許的額外的請求頭

3.通過iframe

沒有特殊情況下,iframe的加載是沒有跨域限制的。<iframe> 載入的任何資源是允許跨域的。我們可以通過幾個手段,讓iframe的內容,傳遞到父窗口中。

1.Window.name + iframe

  • window.name屬性值在文檔刷新后依舊存在的能力(且最大允許2M左右)。
  • 每個iframe都有包裹它的window。
  • contenWindow返回的是<iframe>元素的window對象
// localhost:3001/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
    <script>
    var iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    var state = 0 // 設置狀態防止頁面無限刷新
    iframe.onload = function() {
      if (state == 1) {
        console.log(iframe.contentWindow.name)
        // 清除創建的iframe
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
      } else if (state == 0) {
        state = 1
        iframe.contentWindow.location = 'http://localhost:3001/proxy.html'; //加載完成,iframe指回當前域
        // 防止 Blocked a frame with origin "xxxx" from accessing a cross-origin frame.錯誤
      }
    }
    iframe.src = 'http://localhost:3000/';
    document.body.appendChild(iframe);
  </script>
</body>
</html>

// localhost:3000,server
const koa = require('koa')
// const cors = require('koa2-cors')
const app = new koa()
// app.use(cors())
app.use(async ctx => {
  data = '\'hello world\''
  ctx.body = `
    <h1>${data}</h1>
    <script>
      window.name = ${data}
    </script>
  `;
});
app.listen(3000)

結果: 瀏覽器console 輸出 hello world,表示我們在localhost:3001中拿到了localhost:3000的數據。

三、為什么瀏覽器需要同源策略

考慮一下這個場景:你打開了A網站,并且登錄了A網站,A網站也記錄了你的cookie信息,然后你打開一個B網站,如果沒有同源策略,B網站是可以直接請求A網站的接口的,有一些比如個人信息,他就可以通過get等方法,獲得到你的信息,甚至可以post等操作去修改你的信息,這樣你的賬戶安全是受到很嚴重的威脅的。所以瀏覽器需要同源策略來保證一定的安全,攻擊手段例如CSRF和XSS,下面會詳細講。

四、跨源網絡訪問

1.通常允許跨域寫操作

例如:links,重定向(傳統頁面的跳轉),以及表單提交。特定少數的HTTP請求需要添加 preflight

2.通常允許跨域資源嵌入
  • <script src="..."></script> 標簽嵌入跨域腳本。語法錯誤信息只能在同源腳本中捕捉到。
  • <link rel="stylesheet" href="..."> 標簽嵌入CSS。由于CSS的松散的語法規則,CSS的跨域需要一個設置正確的Content-Type 消息頭。不同瀏覽器有不同的限制: IE, Firefox, Chrome, Safari (跳至CVE-2010-0051)部分 和 Opera
  • <img>嵌入圖片。支持的圖片格式包括PNG,JPEG,GIF,BMP,SVG,…
  • videoaudio嵌入多媒體資源。
  • @font-face 引入的字體。一些瀏覽器允許跨域字體( cross-origin fonts),一些需要同源字體(same-origin fonts)。
  • <iframe> 載入的任何資源。站點可以使用X-Frame-Options消息頭來阻止這種形式的跨域交互。

曾經就遇到過字體文件跨域問題,在引入fontawesome的時候,webpack打包之后上傳到服務器,瀏覽器加載不到字體文件,錯誤顯示了跨域的問題。解決方案需要在nginx配置請求字體文件返回的請求頭

location ~* \.(eot|ttf|woff|svg|otf)$ {
     add_header Access-Control-Allow-Origin *;
}

3.canvas中img的跨域

HTML 規范中圖片有一個 crossorigin 屬性,結合合適的 CORS 響應頭,就可以實現在畫布中使用跨域[圖片上傳失敗...(image-c285c0-1522737772146)]

元素的圖像。

有一次有個需求,就是對頁面進行截圖,我的方案是html2canvas ,canvs2image,結果導致了部分圖片截不下來,其原因就是canvas畫布被污染了。

盡管不通過 CORS 就可以在畫布中使用圖片,但是這會污染畫布。一旦畫布被污染,你就無法讀取其數據。例如,你不能再使用畫布的 toBlob(), toDataURL()getImageData() 方法,調用它們會拋出安全錯誤。

解決辦法對畫布中的圖片配置img.crossOrigin = "Anonymous";

CSRF與XSS

一、XSS

跨站腳本(英語:Cross-site scripting,通常簡稱為:XSS)是一種網站應用程序的安全漏洞攻擊,是代碼注入的一種。它允許惡意用戶將代碼注入到網頁上,其他用戶在觀看網頁時就會受到影響。這類攻擊通常包含了HTML以及用戶端腳本語言。

XSS其實是為了和CSS做區分吧。

在好幾年前,XSS非常流行,可以用XSS獲得用戶的cookie,瀏覽器版本等信息,比如如果使用XSS獲得到了管理員的cookie,那網站可就危險了。隨著行業的發展,XSS越來越受重視,瀏覽器也對這種手段做了一定的預防,比如同源策略?CSP?

1.XSS是什么
反射型

反射型的攻擊需要攻擊者去欺騙用戶點擊,或者腳本當作url參數注入到頁面中。

舉個栗子

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>

<body>
  <script>
    var str = window.location.href.split('injection=')[1]
    document.write('<script>' +
      decodeURIComponent(str) +
      '<\/script>')
  </script>

</html>

瀏覽器輸入url+?injection=alert(1)

這時候會彈出一個彈窗內容為1,這就是XSS注入的一種方式。

存儲型

存儲型比反射型的危害更大,因為存儲型是用戶把攻擊代碼提交到數據庫中,當別的用戶訪問時,數據庫把攻擊代碼返回給用戶,用戶就會受到攻擊。

xss link&svg黑魔法

2.怎么預防XSS
  1. 盡量不在特定地方輸出不可信變量:script / comment / attribute / tag / style, 因為逃脫 HTMl 規則的字符串太多了。
  2. 將不可信變量輸出到 div / body / attribute / javascript tag / style 之前,對 & < > " ' /進行轉義
  3. 將不可信變量輸出 URL 參數之前,進行 URLEncode
  4. 使用合適的 HTML 過濾庫進行過濾。介紹個庫Secure XSS Filters
  5. 預防 DOM-based XSS,見 DOM based XSS Prevention Cheat Sheet
  6. 開啟 HTTPOnly cookie,讓瀏覽器接觸不到 cookie

二、CSRF

1.CSRF是什么

Cross-site request forgery 跨站請求偽造,也被稱為 “one click attack” 或者 session riding,通常縮寫為 CSRF 或者 XSRF,是一種對網站的惡意利用。CSRF 則通過偽裝來自受信任用戶的請求來利用受信任的網站。

2.怎么預防

從它的原理來講,我們的目的就是,不讓已經登錄A網站的用戶,打開B網站后,受到攻擊,也就是說,不讓B網站向A網站發送有效的請求,獲得用戶在A網站的信息或者進行破壞。

Token

1.SSR

大部分SSR的網站都使用form表單進行提交內容的,這時候如果B網站也構造一個和A網站一樣的form表單,一樣可以提交內容到A網站。

我們只需要一個能驗證身份的,能證明這個請求是從A發出來的,而不是B發出來的就可以了。如下面Codeforces的Login

<form method="post" action="" id="enterForm"><input type="hidden" name="csrf_token" value="635faaad128c55d2849b89e80bf790a3">
            <table class="table-form">
                <input type="hidden" name="action" value="enter">
                <input type="hidden" name="ftaa" value="">
                <input type="hidden" name="bfaa" value="">

        <input type="hidden" name="_tta" value="243"></form>

在生成form的時候,后臺插入了一個csrf_token用來驗證請求來源,這個csrf_tokenB網站是拿不到的,所以這樣就能驗證請求是從A發出來的而不是B。

2.SPA

對于SPA,非SSR,無法在form中嵌入一個csrf_token,一般后臺是不會把Origin設置成*的,可以直接限制請求來源,有一點得注意的,請求必須保證不能用form構造出來,比如可以使用json交互。如果后臺偷懶,像我一樣,直接用個中間件沒有設置啥的話,默認是不做跨域限制的。

這時候就將身份驗證信息放在localStorage或者其他地方,總之不要放在cookie中,我們每次請求帶上信息。B網站是讀不到這些信息的,也就無法攻擊了。

SameSite

關于這個SameSite好像使用得非常少。

SameSite-cookies是一種機制,用于定義cookie如何跨域發送。這是谷歌開發的一種安全機制,并且現在在最新版本(Chrome Dev 51.0.2704.4)中已經開始實行了。SameSite-cookies的目的是嘗試阻止CSRF(Cross-site request forgery 跨站請求偽造)以及XSSI(Cross Site Script Inclusion (XSSI) 跨站腳本包含)攻擊。

似乎,只有在chrome瀏覽器才有這個機制。

這個的原理是阻止第三方cookie的發送,比如:B網站向A網站的接口發送請求,是不會帶上A網站已有的cookie的,而A網站向A網站的接口發送請求,還是會帶上cookie的。這是瀏覽器的一種機制。具體的請看參考鏈接中的再見,CSRF:講解set-cookie中的SameSite屬性

檢查來源

驗證referer字段

HTTP頭有一個字段叫做referer它記錄了該 HTTP 請求的來源地址。后臺可以根據這個字段來判斷這個請求是否是從網站A發出的,如果不是,就是不合法的請求。

3.CSRF竊取

利用CSS手段,有很多網站其實是把token放在一個隱藏的input中的,css中有一個input的選擇器,可以匹配出input中以某個字符串開頭,通過選擇器,加載一個外部資源,例如背景圖片。但是這有個前提,需要原來的頁面存在注入,例如文章里這段代碼。

<form action="https://security.love" id="sensitiveForm">
    <input type="hidden" id="secret" name="secret" value="dJ7cwON4BMyQi3Nrq26i">
</form>
<script src="mockingTheBackend.js"></script>
<script>
    var fragment = decodeURIComponent(window.location.href.split("?injection=")[1]);
    var htmlEncode = fragment.replace(/</g,"<").replace(/>/g,">");
    document.write("<style>" + htmlEncode + "</style>");
</script>

從url中的queryString獲取參數,插入到dom中,dom中加載樣式,來猜解token。

查看該demo的源代碼就知道原理了。

demo

參考鏈接

跨域資源共享 CORS 詳解

瀏覽器的同源策略

HTTP訪問控制(CORS)

啟用了 CORS 的圖片

再見,CSRF:講解set-cookie中的SameSite屬性

CSRF 攻擊的應對之道

利用CSS注入(無iFrames)竊取CSRF令牌

內容安全策略( CSP )

XSS 攻擊的處理

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容