68

HTTP的同源策略与跨域资源共享(CORS)机制

 5 years ago
source link: https://www.freebuf.com/articles/web/195925.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

*本文作者: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无法很好防范的原因):

b2au2uf.jpg!web

综上,对于简单跨域请求,若未正确配置则请求正常发送,不能获取返回结果(浏览器拦截)。

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是否可以接收回应。

改造后前端终于能够拿到结果:

q2ay2mn.jpg!web

预检请求

若请求不为简单请求,那么在发起该请求前必须使用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>

同域不存在预检请求:

67VV73Z.jpg!web

跨域出现OPTIONS请求,默认情况下跨域被阻止:

jARfIjb.jpg!web

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字段。

ZVVjqqJ.jpg!web

与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 原创奖励计划,未经许可禁止转载。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK