HTTP的同源策略與跨域資源共享(CORS)機制
*本文作者:x565178035,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。
同源策略
準確的說,同源策略是指,瀏覽器內部在發起如下請求時,該來源必須是當前同源的HTTP資源:
1. 以跨站點的方式呼叫XMLHttpRequest或者Fetch API。 2. Web字型(用於CSS中@ font-face的跨域字型使用) 3. WebGL textures 4. 使用drawImage繪製到canvas的影象/視訊幀。 5. 樣式表(用於CSSOM訪問)
注意:兩個URI同源當且僅當它們的協議://host:port相同。
從第一點可以看到,瀏覽器限制從指令碼內部發起跨域的HTTP請求——更準確的說,同源策略有的限制有兩種表現:(1)限制發起AJAX請求(XMLHttpRequest,Fetch);(2)攔截其他跨站請求的返回結果;這取決於請求是否為簡單請求。
CORS
跨域資源共享(Cross-Origin Resource Sharing, CORS)是一種解決跨域請求的方案,其機制是使用一組額外響應頭(Access-Control-Allow-Origin)和預檢請求(OPTIONS)來使瀏覽器有權使用非同源資源。大部分的現代瀏覽器符合該標準。
簡單請求
若請求滿足所有下述條件,則該請求可視為“簡單請求”:
使用下列方法之一:
GET HEAD POST
並且Content-Type的值僅限於下列三者之一:
text/plain multipart/form-data application/x-www-form-urlencoded
Fetch 規範定義了對 CORS 安全的首部欄位集合,也就是說,不得手動設定除以下集合之外的欄位(否則不為簡單請求)。該集合為:
Accept Accept-Language Content-Language Content-Type DPR Downlink Save-Data Viewport-Width Width
並且請求中的任意 XMLHttpRequestUpload 物件均沒有註冊任何事件監聽器; XMLHttpRequestUpload 物件可以使用 XMLHttpRequest.upload 屬性訪問。
並且請求中沒有使用 ReadableStream 物件。
簡單請求會直接傳送請求而不會觸發預請求,但是不一定能拿到結果,這取決於請求的伺服器Response的Access-Control-Allow-Origin內容。注意以上條件只要有一條不滿足則不為簡單請求。
簡單請求跨域表現
發起請求服務 http://127.0.0.1:8000/ajax.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>AJAX</title> </head> <script> function submitRequest() { var xhr = new XMLHttpRequest(); xhr.open("GET", "http://127.0.0.1:8888/get", true); xhr.withCredentials = true; xhr.send(); xhr.onreadystatechange = function(){ if(xhr.readyState === 4 && xhr.status === 200){ alert(xhr.responseText); } } } </script> <button onclick="submitRequest()">AJAX</button> </html>
非同源服務 http://127.0.0.1:8888/:
from flask import Flask, request, render_template_string, session app = Flask(__name__) app.secret_key='random_secret_key' @app.route('/get', methods=['GET']) def get(): if session.get('user','')=='admin': return "Admin do something!" else: return "No Privilege..." @app.route('/login', methods=['GET']) def login(): user=request.args.get("user", "Null") session["user"]=user template=""" <h3> Login as {{ user }}... </h3> """ return render_template_string(template, user=user) if __name__ == '__main__': app.run(host='127.0.0.1', port=8888, debug=True)
發請求,可以看到請求確實已傳送,並且可以帶cookie(withCredentials),但是js沒有拿到結果:
AJAX請求結果(請求成功,回傳失敗,所以這也是GET型CSRF無法很好防範的原因):
綜上,對於簡單跨域請求,若未正確配置則請求正常傳送,不能獲取返回結果(瀏覽器攔截)。
Origin和Access-Control-Allow-Origin
可以看到在請求中存在Origin欄位,它標記了來源,對應的Access-Control-Allow-Origin為迴應包頭攜帶欄位,它表示那些來源可以訪問本域,*表示所有來源(注意它不能與credentials一起使用)。
使用CORS實現的支援跨域的非同源服務 http://127.0.0.1:8888/:
@app.route('/get', methods=['GET']) def get(): if session.get('user','')=='admin': ret = "Admin do something!" else: ret = "No Privilege..." resp=make_response(ret) resp.headers['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000" resp.headers['Access-Control-Allow-Credentials'] = 'true' resp.headers['Access-Control-Allow-Methods'] = "POST, GET, OPTIONS, PUT, DELETE, PATCH" return resp
其中還有幾個header:
Access-Control-Allow-Credentials:如果請求需要帶cookie,該header必須為true,同時Access-Control-Allow-Origin不能為*,否則同樣拿不到結果; Access-Control-Allow-Methods:允許的請求方式 Origin和Access-Control-Allow-Origin一個為請求攜帶的欄位,一個為迴應攜帶的欄位,瀏覽器以此來判斷js是否可以接收回應。
改造後前端終於能夠拿到結果:
預檢請求
若請求不為簡單請求,那麼在發起該請求前必須使用OPTIONS傳送預驗請求,伺服器允許後才能傳送實際請求(可以猜想這是為了防止CSRF)。
當請求滿足一下任一條件時,該請求為非簡單請求:
使用了下面任一 HTTP 方法:
PUT DELETE CONNECT OPTIONS TRACE PATCH
人為設定了對 CORS 安全的首部欄位集合 之外的其他首部欄位。
Content-Type的值不屬於下列之一:
application/x-www-form-urlencoded multipart/form-data text/plain
請求中的 XMLHttpRequestUpload 物件註冊了任意多個事件監聽器。
請求中使用了 ReadableStream 物件。
預檢請求跨域表現
假設有伺服器 http://127.0.0.1:8888/json:
@app.route('/json', methods=['GET','POST']) def json(): if request.method == 'GET': return render_template('json.html', Evil="Benign") else: if session.get('user','')=='admin': print("session:",session) data=request.json ret='Admin do '+data["action"] else: ret="No Privilege2..." print(ret) return jsonify({'result': ret})
‘templates/json.html’內容為:
<html> <title>{{ Evil }}</title> <center> <h1> Reset Password </h1> <head> <script type="text/javascript"> function submitRequest() { var xhr = new XMLHttpRequest(); xhr.open("POST", "http://127.0.0.1:8888/json", true); xhr.setRequestHeader("Accept", "*/*"); xhr.setRequestHeader("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3"); xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); xhr.withCredentials = true; xhr.send(JSON.stringify({"action":"change passwd..."})); xhr.onreadystatechange = function(){ if(xhr.readyState === 4 && xhr.status === 200){ alert(xhr.responseText); } } } </script> </head> <body> <button onclick="submitRequest()">Conform</button> </body> </html>
同域不存在預檢請求:
跨域出現OPTIONS請求,預設情況下跨域被阻止:
Access-Control-Request-Method:欄位說明請求的操作。
允許跨域請求
在OPTIONS和POST報頭加入Access-Control-Allow-Origin等欄位
@app.route('/json', methods=['GET','POST','OPTIONS']) def json(): if request.method == 'GET': return render_template('json.html', Evil="Benign") else: if session.get('user','')=='admin': print("session:",session) data=request.json ret='Admin do '+data["action"] else: ret="No Privilege2..." resp=make_response(jsonify({'result': ret})) resp.headers['Access-Control-Allow-Origin'] = "http://127.0.0.1:8000" resp.headers['Access-Control-Allow-Credentials'] = 'true' resp.headers['Access-Control-Allow-Methods'] = "POST, GET, OPTIONS, PUT, DELETE, PATCH" resp.headers['Access-Control-Allow-Headers'] = "origin, content-type, accept, x-requested-with" return resp
跨站成功,先發送OPTIONS,再發送POST,注意這兩個報頭必須都存在CORS欄位。
與CORS有關的HTTP頭
請求
Origin:<origin>:表示實際請求的源站 Access-Control-Request-Method: <method>:用於預檢請求,表示真實的請求方法。 Access-Control-Request-Headers: <field-name>[, <field-name>]*:用於預檢請求,表示真實請求所攜帶的首部欄位(從抓包上來看chrome沒有按要求來啊Orz)
響應
Access-Control-Allow-Origin: <origin> | *:允許外域URI Access-Control-Allow-Credentials:false:是否允許瀏覽器讀取response內容(如cookie) Access-Control-Allow-Methods:用於預檢請求響應,表示允許使用的HTTP方法 Access-Control-Allow-Headers:用於預檢請求響應,表示允許攜帶的頭部 Access-Control-Expose-Headers:允許響應時能獲取的其他頭部(在跨域訪問時,XMLHttpRequest物件的getResponseHeader()方法只能拿到一些最基本的響應頭) Access-Control-Max-Age:preflight請求的最大響應時間
參考連結
Cross-Origin Resource Sharing(CORS)詳解,CORS詳解,CORS原理分析, https://www.cnblogs.com/demingblog/p/8393511.html
HTTP訪問控制(CORS), https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
*本文作者:x565178035,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。