浅谈前端“源”相关
0X00 前言
同源策略是前端安全的基石,想学好前端安全的第一步也是最重要的一步就是深入理解同源策略以及和同源策略相关的比如 cors csp jsonp 还有 xss 经常会涉及的 cookie 作用域,以及脚本源的归属问题,原先只是简单的了解,于是打算抽出一点时间再次强化一下对其的理解,以便后期能对前端的安全作出更加深入的分析和探究。
0X01 同源策略
为了下面的解释让人更好理解,我想要先解释一下同源策略
1.什么叫同源
同源,就是协议、域名、端口号相同的URL,但凡这里面有一个不一样,我们就认为是不同源
下面我们来举一个实际的例子帮助我们理解一下
url | 是否同源 | 原因 |
---|---|---|
http://abc.com/other | 是 | 协议、端口、域名相同 |
https://abc.com | 否 | 协议不同(https) |
http://abc.com:81 | 否 | 端口不同(81) |
http://www.abc.com/ | 否 | 域名不同,顶级域名与www子域名不是一个概念 |
http://news.abc.com/ | 否 | 域名不同(news) |
注意:
这里比较容易混淆的就是 abc.com 和 www.abc.com ,我们常常把 www作为主域名,但是实际上是不规范的,www其实也只是 abc.com 的一个子域名而已。
2.同源策略 Same-Origin-Policy(SOP)
浏览器采用同源策略,在没有明确授权的情况下,禁止页面加载或执行与自身不同源的任何脚本。
注意:这里说的明确授权指的是下面会讲到的 cors jsonp 等合理跨域方式
(1)同源策略又分为以下两种:
1.DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的(比如一个恶意网站的页面通过iframe嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的javascript脚本就可以在用户登录银行的时候获取用户名和密码)
2.XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求(这一点里面其实包括了 ajax)。
(2)但是同源策略留了一些“后门”
1.页面中的链接,重定向以及表单提交是不会受到同源策略限制的(未授权情况下,ajax 的表单提交是不被允许的,但是普通的表单是可以直接跨域的)。
2.<script>、<img>、<iframe>、<link>
这些包含 src 属性的标签可以加载跨域资源。但浏览器限制了JavaScript的权限使其不能读、写加载的内容。
3.为什么要有同源策略
很明显,我们是为了解决浏览器浏览中的一些安全问题才出现的同源策略,举一下两个例子
(1)如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:
1.做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank.com。
2.把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
3.这时如果用户输入账号密码,我们的主网站可以跨域访问到 http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。
(2)如果 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:
1.用户登录了自己的银行页面 http://mybank.com,http://mybank.com 向用户的 cookie 中添加用户标识。
2.用户浏览了恶意页面 http://evil.com,执行了页面中的恶意 AJAX 请求代码。
3.http://evil.com 向 http://mybank.com 发起 AJAX HTTP 请求,浏览器会默认把 http://mybank.com 对应 cookie 也同时发送过去。
4.银行页面从发送的 cookie 中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了,而且由于 Ajax 在后台执行,用户无法感知这一过程。
注意:
cookie 是浏览器根据你请求的页面进行发送的,而与从哪个页面发送过去请求无关。比如说,我访问了 a.com 以后获取了 a.com 的cookie ,然后我访问 q.com ,由于 cookie 域的限制,浏览器在请求 q.com 的时候不会携带 a.com 的cookie ,但是当 q.com 有一个向 a.com 发起的请求的时候,浏览器就会自动的在这个请求中带上 a.com 的 cookie。
4.关于同源的一些小结
(1)跨域读: 同源策略是不允许这种事情发生的,如果可以你就能使用 js 读取嵌入在 iframe 中的页面的 dom 元素,获取敏感信息了
(2)跨域写:同源策略不阻止这种操作,比如向不同源的地址发送 POST 请求等,但是这种允许只限制在普通表单(而且是在没有 CSRF token 或者验证 referer 的情况下),对于 ajax 这种方式也是默认不允许的,如果随随便便允许就会出现使用 ajax 来进行 CSRF 请求的情况了
(3)跨域嵌入:这种方式是默认允许的,我们可以在一个源中通过 iframe 嵌入 另一个源的页面,但是如果想限制这种操作的话,我们可以设置 x-frame-options 这个头,这样设置了这个头的页面就允许被嵌入到不同源的页面中了
0X02 跨域的方法
我们知道同源策略的出现是为了保证浏览器浏览的安全性,但是在实际的开发中,我们有些时候必须要对不同源的资源进行访问(比如说使用 ajax 进行跨域的表单提交,再比如说前端开发人员需要调用后端开发人员提供的 api 接口获取数据等),毕竟安全性也不能干扰正常的网站的开发工作,于是在基于同源策略的情况下一些跨域的方式出现了,最主要的方式就是 CORS 和 JSONP
1.CORS(跨域资源共享)
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。
实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)
只要同时满足以下两大条件,就属于简单请求。
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
凡是不同时满足上面两个条件,就属于非简单请求(最常见的就是 ajax 发送 json 数据)。
浏览器对这两种请求的处理方式是不一样的
(1)简单请求
1.在请求中需要附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定给予何种响应。例如:Origin: http://www.K0rz3n.com
2.如果服务器认为这个请求可以接受,就在 Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以回发 * )。例如:Access-Control-Allow-Origin:http://www.K0rz3n.com
3.没有这个头部或者有这个头部但源信息不匹配,浏览器就会驳回这个响应。正常情况下,浏览器会处理该响应。
例如 localhost:63343 通过Ajax请求http://192.168.10.61:8080服务器资源时就会出现如下异常:
注意:
请求和响应都不包含 cookie 信息。如果需要包含 cookie 信息,ajax 请求需要设置 xhr 的属性withCredentials 为 true,服务器需要设置响应头部 Access-Control-Allow-Credentials:true。
(2)非简单请求
浏览器在发送真正的请求之前,会先发送一个 Preflight 请求给服务器,这种请求使用 OPTIONS 方法,发送下列头部:
Origin:与简单的请求相同。
Access-Control-Request-Method: 请求自身使用的方法。
Access-Control-Request-Headers: (可选)自定义的头部信息,多个头部以逗号分隔。
例如:
OPTIONS /someData/ HTTP/1.1
Host: public-data.com
......
Origin: http://ambergarden.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-CUSTOM-HEADER
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览器进行沟通:
Access-Control-Allow-Origin:与简单的请求相同。
Access-Control-Allow-Methods: 允许的方法,多个方法以逗号分隔。
Access-Control-Allow-Headers: 允许的头部,多个方法以逗号分隔。
Access-Control-Max-Age: 应该将这个 Preflight 请求缓存多长时间(以秒表示)。
例如:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://K0rz3n.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-CUSTOM_HEADER
Access-Control-Max-Age: 1728000
......
一旦服务器通过 Preflight 请求允许该请求之后,以后每次浏览器正常的 CORS请求,就都跟简单请求一样了。
请求:
POST /someData/ HTTP/1.1
Host: public-data.com
X-CUSTOM-HEADER: custom_header_value
......
[Payload Here]
响应:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://K0rz3n.com
Content-Type: application/xml
......
[Payload Here]
(3)优缺点对比
优点
1.CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护。
2.支持所有类型的 HTTP 请求。
缺点
1.存在兼容性问题,特别是 IE10 以下的浏览器。
2.第一次发送非简单请求时会多一次请求。
2.JSONP(JSON with Padding)
由于 script 标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过动态创建 script标签,然后利用 src 属性进行跨域,这也就是 JSONP 跨域的基本原理,jsonp 可以说是 json 的一种使用模式,其实 jsonp 我在之前的文章中也介绍过了 传送门。
(1)jsonp 的跨域流程
直接通过下面的例子来说明 JSONP 实现跨域的流程:
1.定义一个 回调函数 handleResponse 用来接收返回的数据
function handleResponse(data) {
console.log(data);
};
2.动态创建一个 script 标签,并且告诉后端回调函数名叫 handleResponse
var body = document.getElementsByTagName('body')[0];
var script = document.gerElement('script');
script.src = 'http://K0rz3n.com/json?callback=handleResponse';
body.appendChild(script);
3.通过 script.src 请求 http://www.K0rz3n.com/json?callback=handleResponse
,
4.后端能够识别这样的 URL 格式并处理该请求,然后返回 handleResponse({"name": "K0rz3n"})
给浏览器
5.浏览器在接收到 handleResponse({"name": "K0rz3n"})
之后立即执行 ,也就是执行 handleResponse 方法,获得后端返回的数据,这样就完成一次跨域请求了。
(2)优缺点对比
优点
1.用于实现跟踪用户点击页面或动态广告曝光次数有较大的优势。
缺点
1.只支持 GET 请求(这个是比较大的一个限制条件)。
2.只能浏览器与服务器的单向通信,因为浏览器不能访问服务器的响应文本。
3.img src 跨域
由于 img 标签不受浏览器同源策略的影响,允许跨域引用资源。因此可以通过 img 标签的 src 属性进行跨域,这也就是 img src 跨域的基本原理。
(1)img src 的跨域流程
直接通过下面的例子来说明图像 Ping 实现跨域的流程:
var img = new Image();
//通过 onload 及 onerror 事件可以知道响应是什么时候接收到的,但是不能获取响应文本
img.onload = img.onerror = function() {
console.log("Done!");
}
//请求数据通过查询字符串形式发送
img.src = 'http://www.K0rz3n.com/test?name=K0rz3n';
(2)优缺点对比
优点
1.用于实现跟踪用户点击页面或动态广告曝光次数有较大的优势。
缺点
1.只支持 GET 请求。
2.只能浏览器与服务器的单向通信,因为浏览器不能访问服务器的响应文本。
4.Webserver Proxy 跨域
浏览器有跨域限制,但是服务器不存在跨域问题,所以可以由服务器请求所要域的资源再返回给客户端。
服务器代理是万能的。
5.document.domain 跨域
(1)document.domain 的跨域流程
对于主域名相同,而子域名不同的情况,可以使用 document.domain 来跨域。这种方式非常适用于 iframe 跨域的情况。
比如,有一个页面,它的地址是 http://www.K0rz3n.com,在这个页面里面有一个 iframe,它的 src 是 http://K0rz3n.com/b.html。很显然,这个页面与它里面的 iframe 框架是不同域的,所以我们是无法通过在页面中书写 js 代码来获取 iframe 中的东西的。
这个时候,document.domain 就可以派上用场了,我们只要把 http://www.K0rz3n.com/a.html 和 http://www.K0rz3n.com/b.html 这两个页面的 document.domain 都设成相同的域名就可以了。但要注意的是,document.domain 的设置是有限制的,我们只能把 document.domain 设置成自身或更高一级的父域,且主域必须相同。例如:a.b.K0rz3n.com 中某个文档的 document.domain 可以设成 a.b.K0rz3n.com、b.K0rz3n.com 、K0rz3n.com 中的任意一个,但是不可以设成 c.a.b.K0rz3n.com ,因为这是当前域的子域,也不可以设成 baidu.com,因为主域已经不相同了。
(2)document.domain 跨域的具体实现
例如,在页面 http://www.K0rz3n.com/a.html 中设置document.domain:
<iframe src="http://www.K0rz3n.com/b.html" id="myIframe" onload="test()">
<script>
document.domain = 'K0rz3n.com'; // 设置成主域
function test() {
console.log(document.getElementById('myIframe').contentWindow);
}
</script>
在页面 http://K0rz3n.com/b.html 中也设置 document.domain,而且这也是必须的,虽然这个文档的 domain 就是 K0rz3n.com,但是还是必须显式地设置 document.domain 的值:
<script>
document.domain = 'K0rz3n.com'; // document.domain 设置成与主页面相同
</script>
这样,http://www.K0rz3n.com/a.html 就可以通过 js 访问到 http://K0rz3n.com/b.html 中的各种属性和对象了。
6.window.name 跨域
(1) window.name 的跨域流程
window 对象有个 name 属性,该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面(不管是相同域的页面还是不同域的页面)都是共享一个 window.name 的,每个页面对 window.name 都有读写的权限,window.name 是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
(2) window.name 跨域的具体实现
通过下面的例子介绍如何通过 window.name 来跨域获取数据的。
页面 http://www.K0rz3n.com/a.html 的代码:
<iframe src="http://K0rz3n.com/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
function test() {//iframe载入 "http://K0rz3n.com/b.html 页面后会执行该函数
var iframe = document.getElementById('myIframe');
//重置 iframe 的 onload 事件程序,此时经过后面代码重置 src 之后,http://www.K0rz3n.com/a.html
//页面与该 iframe 在同一个源了,可以相互访问了
iframe.onload = function() {
var data = iframe.contentWindow.name; // 获取 iframe 里的 window.name
console.log(data); // hello world!
};
//重置一个与 http://www.K0rz3n.com/a.html 页面同源的页面
iframe.src = 'http://www.K0rz3n.com/c.html';
}
</script>
页面 http://K0rz3n.com/b.html 的代码:
<script type="text/javascript">
// 给当前的 window.name 设置一个 http://www.K0rz3n.com/a.html 页面想要得到的数据值
window.name = "hello world!";
</script>
7.location.hash 跨域
(1)location.hash 跨域的流程
location.hash 方式跨域,是子框架具有修改父框架 src 的 hash 值,通过这个属性进行传递数据,且更改 hash 值,页面不会刷新。但是传递的数据的字节数是有限的。
(2)location.hash 跨域的具体实现
页面 http://www.K0rz3n.com/a.html 的代码:
<iframe src="http://K0rz3n.com/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
// iframe载入 "http://K0rz3n.com/b.html 页面后会执行该函数
function test() {
//获取通过 http://K0rz3n.com/b.html 页面设置 hash 值
var data = window.location.hash;
console.log(data);
}
</script>
页面 http://K0rz3n.com/b.html 的代码:
<script type="text/javascript">
//设置父页面的 hash 值
parent.location.hash = "world";
</script>
8.postMessage 跨域
(1)postMessage 跨域的流程
window.postMessage(message,targetOrigin) 方法是 HTML5 新引进的特性,可以使用它来向其它的 window 对象发送消息,无论这个 window 对象是属于同源或不同源。这个应该就是以后解决 dom 跨域通用方法了。
调用 postMessage 方法的 window 对象是指要接收消息的那一个 window 对象,该方法的第一个参数 message 为要发送的消息,类型只能为字符串;第二个参数 targetOrigin 用来限定接收消息的那个 window 对象所在的域,如果不想限定域,可以使用通配符 *。
需要接收消息的 window 对象,可以通过监听自身的 message 事件来获取传过来的消息,消息内容储存在该事件对象的 data 属性中。
(2)postMessage 跨域的具体实现
页面 http://www.K0rz3n.com/a.html 的代码:
<iframe src="http://K0rz3n.com/b.html" id="myIframe" onload="test()" style="display: none;">
<script>
//iframe载入 "http://K0rz3n.com/b.html 页面后会执行该函数
function test() {
// 获取 http://K0rz3n.com/b.html 页面的 window 对象,
// 然后通过 postMessage 向 http://K0rz3n.com/b.html 页面发送消息
var iframe = document.getElementById('myIframe');
var win = iframe.contentWindow;
win.postMessage('我是来自 http://www.K0rz3n.com/a.html 页面的消息', '*');
}
</script>
页面 http://K0rz3n.com/b.html 的代码:
<script type="text/javascript">
// 注册 message 事件用来接收消息
window.onmessage = function(e) {
e = e || event; // 获取事件对象
console.log(e.data); // 通过 data 属性得到发送来的消息
}
</script>
0X03 cookie 的路径和作用域
1.cookie 的路径
Cookie 的路径是在服务器创建Cookie 时设置的,它的作用是决定浏览器访问服务器的某个资源时,
需要将浏览器端保存的哪些Cookie 归还给服务器
如图所示:
如上图所示,浏览器端保存的Cookie 有三个,分别是Cookie1、Cookie2 和Cookie3。
它们三个的访问路径分别为:
/Example/cookie
/Example/
/Example1/cookie
浏览器访问服务器端的路径为:
http://localhost:8080/Example/cookie/a/index.jsp
也就是说index.jsp 页面的访问路径为:
/Example/cookie/a/
该路径包含了Cookie1 和Cookie2 的路径,因此在访问index.jsp 时,浏览器会将Cookie1 和Cookie2
发送给服务器(简单的说就是父路径给子路径)。这就是Cookie 的路径的作用,其中涉及到访问路径。
如果服务器创建Cookie 时没有设置路径,那么该Cookie 的路径是当前资源的访问路径。
例如:在index.jsp 页面中创建了一个Cookie,index.jsp 页面的访问路径为“/Example/“,那么该Cookie 的路径就是 /Example/。如果服务器创建Cookie 时设置了路径,那么Cookie 的路径就是设置的路径,例如:
cookie.setPath(“/Example/cookie”),那么该Cookie 的路径就是/Example/cookie。
2.cookie 的作用域
Cookie 还有一个属性就是域,Cookie 类中有设置和获取cookie 域的方法,如下所示:
(1)Void setDomain(String pattern):设置cookie 的域;
(2)String getDomain():获取cookie 的域,返回值的类型是String 类型。
一般我们很少使用cookie 的域,只有在多个二级域共享Cookie时才用。例如:www.baidu.com、zhidao.baidu.com、news.baidu.com、tieba.baidu.com 这些域可以理解是百度的子项目,现在要在这些域中共享cookie,就需要使用cookie 的域.
使用时需要注意以下两点:
(1)设置domain 为:setDomain(“.baidu.com”);
(2)设置path 为:setPath(“/”)。
0X04脚本源的归属问题
关于脚本源的归属问题是这样规定的,脚本的源是由它所嵌入的页面决定的,也就是说,如果 利用 xss 漏洞向某个页面注入了 js 代码,那么这段 js 代码就是属于这个网站所属的源的,就能对这个网站的内容进行操作,比如说读取 dom 的内容等,这是非常危险的,因为浏览器没有能力去鉴别这段代码是不是网站所有者自己写进网页的,于是就引出了 CSP 这个东西.
0X05 CSP
一个 编写 CSP 的网站 https://www.cspisawesome.com/
1.什么是CSP
全称:Content Security Policy 内容安全策略,管理员可以定义多种策略的范围,这些策略可以被任意组合,不一定要实现全部
CSP定义了Content-Security-Policy HTTP头来允许你创建一个可信来源的白名单,使得浏览器只执行和渲染来自这些来源的资源,而不是盲目信任服务器提供的所有内容。即使攻击者可以找到漏洞来注入脚本,但是因为来源不包含在白名单里,因此将不会被执行。
2.CSP 包含哪些内容
(1)内容源
大多数的策略指令都需要一个或者多个内容源,这些内容源指明内容应该从什么地方加载
(2)源列表
指定主机 主机可以使用 通配符,主机与主机之间使用空格分割
(3)关键字
关键字用来描述某些特别的内容源
1.’none’ :不匹配任何的URL,两侧的单引号必须
2.’self’ :与文档同源
3.’unsafe-inline’ :允许使用内联资源,包括内联的<script>
内联的 javascript:URL 内联的事件处理函数 onerror 和内联的<style>
4.’unsafe-eval’:允许通过字符串创建代码的方式
注意: unsafe-inline 和 unsafe-eval 都是不安全的,他会使网站有跨站脚本的风险
(4)数据
data:允许使用 data:URL 是不安全的
mediatream:将 mediastream:URL 作为内容源
两个实例:
Content-Security-Policy: default-src 'self' trustedscripts.foo.com
Content-Security-Policy: default-src 'self'; img-src 'self' data:; media-src mediastream:
(5)指令
1.base-uri
base-uri 指令定义了 URI,它可以作为文档的基准 URL。如果没有指定值,那么任何 URI 都被允许。如果没有指定这条指令,浏览器会使用 base 元素中的 URL。
base-uri source-list
2.child-src
child-src 指定定义了 web workers 以及嵌套的浏览上下文(如 <frame>
和 <iframe>
)的源。
child-src source-list
3.connect-src
connect-src 指令定义了请求、XMLHttpRequest、WebSocket 和 EventSource 的连接来源。
connect-src source-list
4.default-src
default-src 指令定义了那些没有被更精确指令指定的(默认)安全策略。该指令包含了以下指令:
child-src
connect-src
font-src
img-src
media-src
object-src
script-src
style-src
default-src source-list
5.img-src
img-src指令指定图像和图标的有效来源。
img-src source-list
6.script-src
script-src指令指定JavaScript的有效源。当包含script-src或default-src指令时,除非分别指定“unsafe-inline”和“unsafe-eval”,否则内联脚本和eval()将被禁用。
7.style-src
style-src指令指定样式表的有效来源。这包括外部加载的样式表和<style>
元素和HTML样式属性的内联使用。来自源列表中不包含的源的样式表不会被请求或加载。当包含style-src或default-src指令时,<style>
元素和HTML样式属性的内联使用将被禁用,除非您指定’unsafe-inline’。
8.object-src
object-src指令为<object>
,<embed>
,<applet>
元素指定有效的源。
object-src source-list
0X06 总结
这篇文章主要是对一些前端或者浏览器中涉及到和 “源” 有关的一些东西的罗列和整理吧,很多东西来源于网络,夹杂了一些我个人的理解,全当做后期的备忘来用吧
0X07 参考
https://yq.aliyun.com/ziliao/317883
https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
https://blog.csdn.net/shimiso/article/details/21830313
https://blog.csdn.net/wonking666/article/details/79159180
https://www.cnblogs.com/renpingsheng/p/7688134.html
https://www.w3.org/Security/wiki/Same_Origin_Policy
https://w3c.github.io/webappsec-csp/
https://security.stackexchange.com/questions/8264/why-is-the-same-origin-policy-so-important
https://security.stackexchange.com/questions/91165/why-is-the-synchronizer-token-pattern-preferred-over-the-origin-header-check-to
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
https://lorexxar.cn/2016/08/08/ccsp/
https://lorexxar.cn/2018/04/05/0ctf2018-blog/
https://www.cnblogs.com/laixiangran/p/9064769.html