同源策略与跨域请求
Source & Reference
教程视频:JSONP+CORS+服务器代理
参考文档:
- 同源策略(没有找到文档出处)
- 同源策略与跨域请求(作者:晚风)
0x00 SOP的介绍
同源策略(Same origin policy)是一种约定,是浏览器最核心也是最基本的功能,可以说WEB是构建在同源策略的基础上的
同源策略的核心就是:它认为来自任何站点装载的信赖内容都是不安全的,当被浏览器半信半疑的脚本运行在沙箱时,它们只被允许访问来自同⼀站点的资源,而不是那些来自其它站点可能怀有恶意的资源。
简单来说,就是只有IP、协议、端口相同的页面才会被认为是同源的,而只有被认为同源的页面才不会被跨域限制
另外,同源策略(跨域限制)又分为两种:
- DOM同源策略:禁止不同源的页面DOM进行操作,主要针对iframe跨域的情况
- XMLHttpRequest同源策略:禁止使用XHR对象向不同源的服务器发起HTTP请求
我们换一个角度看待问题,可以看作一些操作受到同源策略的限制
- 无法读取非同源页面的Cookie、sessionStorage、localStorage、IndexedDB
- 无法读写非同源网页的DOM
- 无法向非同源地址发送AJAX请求:可以发送,但会报错
我们现在已经了解了同源策略的限制,接下来我们需要知晓跨域限制的必要性
- 如果没有DOM同源策略:hacker可以制作假网站,诱导用户登录,登录完成后直接用其登录的数据访问真实网站,这样就可以在不被用户察觉的情况下拿到用户的账密
- 如果没有XMLHttpRequest同源策略:hacker构造的恶意网站,可以直接使用用户登录其它网站的凭证进行操作,即可以轻易的进行CSRF攻击
0x01 前端跨域方法
业务环境种不可避免的会出现一些跨域的需求,如:电商网站想通过用户浏览器加载第三方快递网站的物流信息
① - JSONP跨域
原理:由于<script>
不受浏览器同源策略的影响,允许跨域调用资源,因此可以通过动态创建script标签,然后利用src属性进行跨域
有点难理解,我们直接上源码,再理解不了,就去看视频
流程示例
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<script>
function test(data) {
alert(data);
}
</script>
<script src="http://localhost/temp/index.php?callback=test"></script>
</body>
</html>
<?php
header("Content-type: application/json");
$callbackGet = $_GET['callback'];
$data = 'Hi, H1kki~';
echo $callbackGet . "('$data')";
- HTML页面通过
<script>
的src属性访问远程PHP脚本,并将需要调用的JS函数名作为参数传递 - PHP脚本在接收到GET传参之后,构造输出需要调用的JS函数,其中函数的传参可以是PHP脚本中的数据
- HTML页面在收到PHP的输出值后,将其作为JS脚本数据解析执行,即成功将远程PHP脚本的数据作为JS脚本函数的传参来执行
② - HTML标签跨域
原理:<script>
、<img>
、<iframe>
、<link>
等带src属性的标签都可以跨域加载资源,而不受同源策略的限制。每次加载时都会由浏览器发送⼀次GET请求,通过src属性加载资源,浏览器会限制JavaScript的权限,使其不能读写返回的内容。
常见标签
<script src="..."></script>
<img src="...">
<video src="..."></video>
<audio src="..."></audio>
<embed src="...">
<frame src="...">
<iframe src="..."></iframe>
<link rel="stylesheet" href="...">
<applet code="..."></applet>
<object data="..." ></object>
流程实例
var img = new Image();
// 通过 onload 及 onerror 事件可以知道响应是什么时候接收到的,但是不能获取响应⽂本
img.onload = img.onerror = function() {
console.log("Done!") ;
}
// 请求数据通过查询字符串形式发送
img.src = 'http://www.xxxx.cn/test?name=xxx';
③ - document.domain 跨域
局限性:适用于顶级域名相同的两个页面中的跨域访问,常用于iframe
跨域的情况
document.domain
:只能设置为以当前页面为子域的主域,默认为当前页面的域名。当且仅当两个页面的document.domain
一致时,它们可以进行相互访问。
示例
<iframe src="http://localhost:81/b.php" i d="iframepage" width="100%"
height="100%" frameborder="0" scrolling="yes" onLoad="getData"> </iframe>
<script>
window.parentDate = {
"name": "hello world!",
"age": 1 8
}
// 使⽤document.domain解决iframe⽗⼦模块跨域的问题
let parentDomain = window.location.hostname;
console.log("domain",parentDomain); //localhost
document.domain = parentDomain;
</script>
④ - window.name 跨域
window.name
:在JS的window对象中有一个name属性,该属性有一个特征:即一个窗口(window)的生命周期内,窗口载入的所有页面(无论同不同域)都是共享一个window.name
的,并且每个页面都对window.name
具有读写权限,且window.name
是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置
示例
<!-- http://localhost/temp/a.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<iframe src="http://localhost/temp/b.html" id="target" onload="test()" style="display: none"></iframe>
<script>
function test() {
const iframe = document.getElementById("target");
const data = iframe.contentWindow.name;
alert(data);
}
</script>
</body>
</html>
<!-- http://localhost/temp/b.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
window.name = "hello, h1kki~"
</script>
</body>
</html>
Tips:可以利用window.name
来缩短任意长度的XSS
⑤ - window.postMessage 跨域
window.postMessage
:是HTML5时代新出现的API,用于安全的进行跨域请求,实现不同页面中的跨域通信
示例
<!-- http://localhost/temp/a.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<iframe src="http://localhost/temp/b.html" id="target" onload="test()" style="display: none"></iframe>
<script>
function test() {
const iframe = document.getElementById("target");
const win = iframe.contentWindow;
// postMessage(需要传递的消息, 限定接收消息的window对象所在的域)
win.postMessage("Hi, I'm a.html !", '*');
}
</script>
</body>
</html>
<!-- http://localhost/temp/b.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
window.onmessage = function (e) {
e = e || event; // 获取事件对象
if (e.origin !== 'http://localhost') {
console.log('origin error!');
return;
}
console.log(e.data); // 通过 data 属性得到发送来的消息
}
</script>
</body>
</html>
⑥ - location.hash 跨域
**location.hash
**方式跨域,是子框架修改父框架src的hash值,通过这个属性进行传递数据,且更改hash值,页面不会刷新
局限:传递的数据的字节数是有限的
<!-- http://localhost/temp/a.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<iframe src="http://localhost/temp/b.html" id="target" onload="test()" style="display: none"></iframe>
<script>
function test() {
const data = window.location.hash;
alert(data);
}
</script>
</body>
</html>
<!-- http://localhost/temp/b.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script>
parent.location.hash = "hello, h1kki~"
</script>
</body>
</html>
⑦ - 浏览器 SOP Bug
虽然所有的浏览器都有同源策略,但是各家浏览器实现的方式也是各不相同。难免实现也会有漏洞。我 们可以找出浏览器同源策略的漏洞来实现跨域访问。
例如,浏览器对CSS的松散解析就会导致跨域bug的出现,详见 → 9877 - Security: cross domain thefts via CSS string property injection - chromium
0x02 后端跨域方法
① - 服务器代理
浏览器有跨域限制,但是服务器不存在跨域问题,所以可以由服务器请求所有域的资源再返回给客户端。
② - CORS(跨域资源共享)
CORS是一个W3C标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。其原理为:使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是否成功。
实现CORS的关键是Server端,只要Server实现了CORS接口,就可以跨源通信;而浏览器会自动完成整个CORS过程,不需要用户参与
浏览器将CORS的请求分为两类——
如果满足以下两个条件,就属于简单请求:
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
否则就属于非简单请求
简单请求
在请求中需要附加⼀个额外的 Origin
头部,其中包含请求页面的源信息(协议、端口、域名),Server通过这个字段信息来决定是否给予响应 —— 如果Server决定响应,则就在Access-Control-Allow-Origin
头部中回发相同的源信息(*
为公共资源);反之,浏览器驳回请求
非简单请求
浏览器在发送真正的请求之前,会先发送⼀个 Preflight
请求给服务器,这种请求使用OPTIONS
方法,发送下列头部:
Origin
:请求页面的源信息Access-Control-Request-Method
:请求自身使用的方法Access-Control-Request-Headers
[可选]:自定义的头部信息,多个头部以逗号分隔
# 示例
Origin: http://www.xxx.cn
Access-Control-Request-Method: POST
Access-Control-Request-Headers: NCZ
发送这个请求后,服务器可以决定是否允许这种类型的请求。服务器通过在响应中发送如下头部与浏览 器进行沟通:
Access-Control-Allow-Origin
:回发相同的源信息(*
为公共资源)Access-Control-Allow-Methods
:允许的⽅法,多个⽅法以逗号分隔Access-Control-Allow-Headers
:允许的头部,多个⽅法以逗号分隔Access-Control-Max-Age
:应该将这个Preflight
请求缓存多长时间(s)Access-Control-Allow-Credentials
:是否允许请求带有验证信息Access-Control-Expose-Headers
:允许脚本访问的返回头,请求成功后,脚本可以在 XMLHttpRequest 中访问这些头信息
# 示例
Access-Control-Allow-Origin: http://www.xxx.cn
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: NCZ
Access-Control-Max-Age: 1728000
⼀旦服务器通过 Preflight
请求允许该请求之后,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样了
③ - flash
Flash有自己的一套安全策略,服务器可以通过crossdomain.xml
文件来声明能被哪些域的SWF文件访问,SWF也可以通过API确定自身能够被哪些域的SWF加载。
简单来说,如果 A → B,需要满足如下条件——
- B站下的
crossdomain.xml
文件存在 - B站下的
crossdomain.xml
中设置了允许A站访问
接下来举一个公共资源的crossdomain.xml
示例
<?xml version="1.0"?>
<cross-domain-policy>
<allow-access-from domain="*" / >
</cross-domain-policy>
如果不想域内的⽂件被其他任何域都能访问到,那么这种做法是不推荐的。正确的做法应该是明确指定本域内的⽂件能被哪些域访问。