讲清 CORS:现代网站的安全跨域配置
CORS 被误解的频率不亚于被配错的频率。本文讲清这些响应头到底做什么、“通配符 + 凭据”的陷阱,以及如何正确锁好一个 API。
跨域资源共享(CORS)往往是很多团队第一次接触浏览器安全的地方——通常是因为某处坏了,而最快的搜索结果说“直接允许所有来源就行”。这个修法确实管用,却悄悄变成了漏洞。本文讲清 CORS 真正在做什么,以及如何在不把 API 向全网开放的前提下配置它。
CORS 实际控制的是什么
CORS 不是服务端的访问控制。它是一种浏览器机制,决定在来源 A 上运行的 JavaScript 是否被允许*读取*来源 B 的响应。它对来自 curl、服务器,或任何无视浏览器的脚本的直接请求毫无约束。把它当作面向浏览器客户端的护栏,而真正的授权无论如何都要放在服务端。
“通配符 + 凭据”的陷阱
最危险的单一 CORS 配置,就是回显请求来源的同时还允许携带凭据:
Access-Control-Allow-Origin: https://evil.example
Access-Control-Allow-Credentials: true如果你的服务器把收到的任何 Origin 原样回显,并设置 Allow-Credentials: true,那么任何网站都能以你已登录的用户身份发起带认证的请求并读取响应。正因如此,浏览器禁止字面量 * 与凭据同时使用——但回显来源绕过了这道保护,又把洞重新打开了。
一个正确、受限的配置
先把进来的来源与一份明确的白名单匹配,然后才回显它:
const allowed = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
const origin = req.headers.get('origin');
if (origin && allowed.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
}Vary: Origin 头容易被忘记,却很重要:没有它,缓存可能把某个来源的允许头返回给另一个来源。
方法和请求头也要收紧
Access-Control-Allow-Origin 只是策略的一半。其余部分也要收紧:
- 只允许该端点实际使用的方法,例如
Access-Control-Allow-Methods: GET, POST。 - 只允许你预期的请求头,而不是一股脑全放。
- 设置合理的
Access-Control-Max-Age,让预检请求被缓存。 - 绝不要把
Allow-Origin: *与Allow-Credentials: true组合在一起。
什么时候用通配符其实没问题
一个真正公开、无需认证、只读的 API——比如汇率数据源、公开的状态端点——可以安全地使用 Access-Control-Allow-Origin: *,正因为它没有凭据、也没有任何私密内容会泄露。危险只来自 * 式的开放与认证的组合。
用真实的跨域环境来测试
CORS 对简单请求和预检请求的行为不同,所以要从一个不同来源的浏览器里同时测试这两种,而不是只用 curl(它完全无视 CORS)。扫描会把你的 CORS 响应头与其余安全检查一起自动检测;想了解 CORS 结果如何纳入有优先级的修复清单,请读如何读懂网站安全扫描报告。