加上 Access-Control-Allow-Origin 之后 API 能用了,并不代表它就安全。CORS 不是加上一次就不用再管的东西,关键在于把许可的对象收紧。本文讲解它的原理和安全的配置方法(不会写出攻击步骤)。
先讲原理
浏览器有一条同源策略:其他源的 JS 不能擅自读取其他网站的响应。CORS 就是用来把这条规则的例外只对服务器明确许可的对象打开的机制。
源(origin)是协议(https)、主机名和端口的组合。https://example.com 与 https://api.example.com 主机不同,所以是不同的源;http://example.com 与 https://example.com 协议不同,也是不同的源。
| 响应头 | 含义 |
|---|---|
Access-Control-Allow-Origin | 允许哪个源读取响应 |
Access-Control-Allow-Credentials | 是否允许带 Cookie 等认证信息的通信 |
Access-Control-Allow-Methods / -Headers | 允许的 HTTP 方法/头 |
关键在于,CORS 是属于「放宽限制」那一侧的机制。放宽的方式一旦出错,本不该被读取的数据就会被第三方读到。
还有一点很重要:CORS 阻止的是 JavaScript 读取响应。简单请求即使没有 CORS 许可,也会到达服务器并被处理。对于使用自定义请求头等情况,浏览器会先用 OPTIONS 请求确认(预检),不被允许时就不发送真正的请求。
与相似机制的区别
CORS 常被与名称相近的机制或密切相关的漏洞混淆。
| 术语 | 决定什么 | 由谁执行 |
|---|---|---|
| 同源策略 | 其他源不能读取响应(浏览器的默认行为) | 浏览器 |
| CORS | 作为上述规则的例外,允许哪些源读取 | 服务器声明,浏览器执行 |
| CSRF | 让用户处于登录状态的浏览器发送非本意请求的攻击 | 用 CSRF 令牌和 SameSite 防御 |
| CSP | 自家页面可以加载哪些脚本、连接哪些地址 | 服务器声明,浏览器执行 |
CORS 关心的是「响应能否被读取」,CSRF 关心的是「能否以用户的名义发送请求」。收紧 CORS 并不能防止 CSRF。
哪些配置是危险的
危险的配置(容易犯)
- 把请求的 Origin 原样反射回去并予以许可
Access-Control-Allow-Origin: *+ 同时允许认证信息- 把不必要的 API 也大范围地对其他源开放
安全的配置
- 白名单(只放可信任的固定源)
- 其他一律默认拒绝
- 带认证信息时不用
*,而指定具体的源
配置错误通常是怎样产生的
大多数配置错误,源于想让 CORS 错误消失。典型的过程如下:
- 前端带着 Cookie 调用 API 时出现 CORS 错误
- 加上
Access-Control-Allow-Origin: *。但对于带认证信息的请求,浏览器不允许页面读取*的响应,所以错误依然存在 - 改成把收到的
Origin原样返回,错误消失了。与此同时,任何网站都能读取已登录的响应
即使使用了白名单,比对方式草率也会导致同样的结果。
- 按「以
example.com结尾」比对,会把evil-example.com这样的其他域名也放进来 - 按「以
https://example.com开头」比对,会把https://example.com.evil.net也放进来 - 为了开发方便而允许
null源,其他网站也能制造出null源来读取响应
请对整个源(协议、主机和端口)做完全一致的比对。
安全配置的基本原则
采用白名单方式(最重要)
不要不加判断地反射 Origin
Origin 原样 echo 到 Access-Control-Allow-Origin。只有与白名单比对一致时才返回。由于返回值会随请求变化,还要同时发送 Vary: Origin,避免缓存混用响应。带认证信息时不要用 *
Access-Control-Allow-Origin: * 与 Allow-Credentials: true 同时使用。要指定具体的源。把开放范围降到最小
* 只用于不需要登录的公开数据。如何检查自己的网站
向自家 API 发送一个声称来自其他源的请求,查看响应头。请只对自己管理的 API 这样做。
# 声称来自未被允许的源,只查看响应头
curl -s -o /dev/null -D - -H "Origin: https://example.com" https://your-api.example/ | grep -i "^access-control"- 如果返回
Access-Control-Allow-Origin: https://example.com,说明 API 在反射 Origin。请加上白名单比对 - 如果同时出现
Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true,请重新审视配置 - 如果什么都没有返回,或只返回自家前端的源,就没有问题
再用 Origin: null 重复同样的检查。对于已验证所有权的网站,网站安全诊断会一并检查 Origin 反射和认证信息相关的配置。其他安全响应头可以用安全响应头诊断确认。
出处(一手资料)
- MDN Web Docs, "Cross-Origin Resource Sharing (CORS)" — developer.mozilla.org(带认证信息的请求不能用 *、预检的触发条件、Vary: Origin)
- WHATWG Fetch Standard, "CORS protocol" — fetch.spec.whatwg.org
- OWASP HTML5 Security Cheat Sheet — cheatsheetseries.owasp.org(只允许可信任的域名;不要未经检查就返回 Origin 头)
- PortSwigger Web Security Academy, "Cross-origin resource sharing (CORS)" — portswigger.net(白名单比对错误、null 源、CORS 不是 CSRF 防护)
- MITRE CWE-942: Permissive Cross-domain Policy with Untrusted Domains — cwe.mitre.org