同源

  • 协议相同 (同为http或https)

  • 域名相同

  • 端口相同

三者之间是与关系

| http://www.example.com/dir/page.html | 原始网址 | | —————————————– | —————- | | http://www.example.com/dir2/other.html | 同源 | | http://example.com/dir/other.html | 不同源(域名不同) | | https://www.example.com/dir/page.html | 不同源(协议不同) | | http://www.example.com:8080/dir/page.html | 不同源(端口不同) |

目的

保证用户信息的安全,防止恶意的网站窃取数据

限制范围

  • cookie、LocalStorage、IndexDB
  • DOM
  • AJAX请求

绕过

正常情况下只有同源的网页才能共享cookie,比如http://a.example.com/a.html的cookie在http://b.example.com/b.html上是拿不到的。但是如果设置相同document.domain两个页面就能共享cookie

复现不出来

iframe

如果两个网页不同源,就拿不到对方的DOM,例如

跟上面cookie一样,如果设置相同的document.domain,就能规避同源策略

b.html中加入<script>document.domain="0e1.cc"</script>

然而,对于完全不同源的网站,document.domain也没有办法,所以目前有三种办法来解决

0x00. 片段识别符

片段识别符和之前的部分以#分隔,就是上图fragment部分,改变片段识别符页面不会刷新,基于这个特性,就可以通过fragment来传递信息

b.html的代码修改为

1
2
3
4
5
6
7
<script>
window.onhashchange = checkMessage;
function checkMessage() {
  var message = window.location.hash;
  alert(message);
}
</script>

对iframe的src进行修改

0x01. windows.name

只要在一个窗口中,window.name这个属性对所有网页是共享的

b.html的代码改为

1
2
3
4
5
<h1>b.test.com</h1>
<script>
window.name="hello b";
location="http://a.0e1.cc/a.html";
</script>

window.postMessage

HTML5为了解决跨域通信的问题,引入了跨文档通信API。这个API引入了window.postMessage方法window.postMessage()方法可以安全地实现跨域通信

b.html的代码改为

1
2
3
4
5
6
<h1>b.test.com</h1>
<script>
window.addEventListener('message', function(e) {
  alert(e.data);
},false);
</script>

AJAX

根据同源策略,AJAX只能将数据发到同源网站内。但是还是有办法进行跨域

  • JSONP
  • WebSocket
  • CORS

JSONP

主要操作是添加一个<script>,向服务器请求JSON数据,服务器收到请求后,把数据放到一个指定名字的回调函数里传回来。这种操作不受同源策略限制

a.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<h1>a.0e1.cc</h1>
<script>
function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://b.test.com/b.html?callback=foo');
}

function foo(data) {
  alert('Your public IP address is: ' + data.ip);
};
</script>

b.html

1
foo({"ip":"8.8.8.8"});

WebSocket

WebSocket是一种通信协议,这个协议不实行同源策略,只要服务器支持,就能进行跨域通信

首先编写一个websocket服务端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
	"github.com/gorilla/websocket"
	"log"
	"net/http"
	"strings"
)

var clients = make(map[*websocket.Conn]bool)
var broadcast = make(chan Message)
var originList = []string{"http://b.test.com", "http://a.0e1.cc"}
var upgrader = websocket.Upgrader{
	CheckOrigin: func(req *http.Request) bool {
		origin := req.Header.Get("Origin")
		for _, o := range originList {
			if origin == o {
				return true
			}
		}
		return false
	},
}

type Message struct {
	Msg string `json:"msg"`
}

func main() {
	http.HandleFunc("/ws", handleConnections)
	err := http.ListenAndServe(":8000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func handleConnections(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer ws.Close()

	clients[ws] = true

	for {
		var msg Message
		err := ws.ReadJSON(&msg)
		if err != nil {
			log.Println(err)
			delete(clients, ws)
			break
		}

		msg.Msg = strings.Replace(msg.Msg, "你","我", -1)
		msg.Msg = strings.Replace(msg.Msg, "吗","!", -1)

		err = ws.WriteJSON(msg)
		if err != nil {
			log.Println(err)
			delete(clients, ws)
			break
		}
	}
}

a.0e1.cc上向b.test.com发送websocket连接

CORS

有三类行为类型会触发同源策略

  • Cross-Origin read
  • Cross-Origin writes
  • Cross-Origin embedding

一般情况下Cross-Origin read是被禁止的,其余两种是允许的。Cross-Origin write中的重定向、links(<a>)、表单提交,Cross-Origin embedding中的资源嵌入,例如<script src="…"></script>,更多的例子可以查看MDN的文档

虽然能够在标签中使用不同源的资源,但是在标签内,比如<script>,想要获取外域资源就会收到同源策略的限制。为了解决这个问题,W3C就提出了CORS

CORS(Cross-Origin Resource Sharing)跨域资源分享,是跨域AJAX的根本解决方法,允许任何类型的请求。浏览器一旦发现AJAX请求跨域,就会自动添加一些附加的头信息,有时还会多出一次附加的请求(OPTIONS)

CORS将请求分为两类simple request和not-so-simple request

区分simple request跟not-so-simple request的方法是,simple request满足以下条件

请求方法是HEADGETPOST之一,HTTP的头信息不超出以下几种字段

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

如果是simple request,浏览器会在这次AJAX请求头里添加一个Origin字段,用来说明本次请求来自哪个源

如果Origin指定的域名在许可的范围内,服务器返回的响应头里会多出几个字段

例如上图的Access-Control-Allow-Origin,如果该字段是*,表示接受任意域名的请求

还有就是Access-Control-Expose-Headers,CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定

还有就是Access-Control-Allow-Credentials,如果为true,表示服务器允许把Cookie包含在请求中发送给服务器

当设置了Access-Control-Allow-CredentialsAccess-Control-Allow-Origin就不能为*

对于not-so-simple request,浏览器将会在跨域之前发起一个preflight请求进行检查,如果检查不通过,那么不会发起跨域请求。

image.png

首先会发起一个OPTIONS请求

image.png

这回多了两个头,Access-Control-Request-Method,列出浏览器的CORS请求用到了哪些方法;Access-Control-Request-Headers,指定浏览器CORS请求会额外用到哪些头

收到OPTIONS请求后,服务器检查字段后,允许跨域请求,就会做出回应

image.png

Access-Control-Allow-Methods表示服务器所支持的跨域请求方法

Access-Control-Allow-Headers表示服务器所支持的头

还有就是Access-Control-Max-Age,表示这次OPTIONS请求的有效期

通过浏览器的检查后就可以正常进行跨域请求了

image.png

参考

https://www.jianshu.com/p/2c07fbb52b45

http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html

http://www.ruanyifeng.com/blog/2016/04/cors.html

https://lightless.me/archives/review-SOP.html

https://xz.aliyun.com/t/4470