背景
跨域這個問題前端開發者都接觸過,網上的文章也非常多,但是昨天的騰訊二面給我留了非常深刻的印象,原來跨域能問出那么多花樣,難怪所有面試官都喜歡和面試者來探討這個問題。
跨域
一、什么是跨域
簡單的來說就是瀏覽器限制了向不同域發送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-urlencoded
、multipart/form-data
、text/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,… -
video
和audio
嵌入多媒體資源。 -
@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注入的一種方式。
存儲型
存儲型比反射型的危害更大,因為存儲型是用戶把攻擊代碼提交到數據庫中,當別的用戶訪問時,數據庫把攻擊代碼返回給用戶,用戶就會受到攻擊。
2.怎么預防XSS
- 盡量不在特定地方輸出不可信變量:script / comment / attribute / tag / style, 因為逃脫 HTMl 規則的字符串太多了。
- 將不可信變量輸出到 div / body / attribute / javascript tag / style 之前,對
& < > " ' /
進行轉義 - 將不可信變量輸出 URL 參數之前,進行 URLEncode
- 使用合適的 HTML 過濾庫進行過濾。介紹個庫Secure XSS Filters
- 預防 DOM-based XSS,見 DOM based XSS Prevention Cheat Sheet
- 開啟 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的源代碼就知道原理了。