CORS Security
CORS(Cross-Origin Resource Sharing,跨源资源共享)是现代浏览器实现的一种安全机制, 它控制 Web 应用程序如何向与提供原始页面的域不同的 域发起 HTTP 请求,以受控的方式放宽了 Same-Origin Policy (SOP)—— 一项基本的安全策略,它限制某一源的脚本访问 另一源的资源。尽管 CORS 对于现代 Web 应用程序架构至关重要,其中 前端经常需要消费托管在不同域上的 API,但其不当 配置是当代 Web 应用程序中最常见、最危险的漏洞之一。 CORS 配置错误可能将敏感数据暴露给未经授权的域, 即使存在反 CSRF 令牌也能允许跨站请求伪造 (CSRF) 攻击, 助长凭据窃取,并在极端情况下允许攻击者代表已认证用户执行 特权操作。问题因以下事实而加剧:许多 开发人员在开发过程中遇到 CORS 错误时,会选择过于宽松的 解决方案(例如使用通配符 "*" 或自动反射请求的源),而未能充分理解 其安全影响。本文深入探讨 CORS 的基础知识、 常见的配置漏洞,并确立在不同平台和框架上进行安全实现的 稳健实践,在功能性与适当的防御态势之间取得平衡。
Same-Origin Policy (SOP)
浏览器实现 SOP:脚本只能访问同源的资源 (协议 + 域名 + 端口)。CORS 以受控的方式放宽 SOP。
# 同源
https://example.com/api ← https://example.com/app [OK]
# 不同源(被 SOP 阻止)
https://example.com ← http://example.com (协议)
https://example.com ← https://api.example.com (子域名)
https://example.com ← https://example.com:8080 (端口)
CORS Headers
Access-Control-Allow-Origin
# 允许特定源(推荐)
Access-Control-Allow-Origin: https://trusted.com
# 允许任何源(危险!)
Access-Control-Allow-Origin: *
# 基于白名单的动态配置(正确)
const allowedOrigins = ['https://app1.com', 'https://app2.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
其他重要的 Headers
# 允许凭据(cookies、auth headers)
Access-Control-Allow-Credentials: true
# 允许的 HTTP 方法
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# 请求中允许的 headers
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
# 向客户端 JavaScript 暴露的 headers
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
# 预检请求的缓存时间(秒)
Access-Control-Max-Age: 86400
Preflight Requests
浏览器在发起“非简单”请求之前会发送 OPTIONS 请求以验证权限。
# 客户端发送预检请求
OPTIONS /api/resource HTTP/1.1
Origin: https://app.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
# 服务器返回权限响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization
Access-Control-Max-Age: 86400
常见的 CORS 漏洞
1. 通配符与凭据并用
# [错误] 易受攻击——无效且危险
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
# 浏览器会阻止此组合
# [OK] 正确——特定源配合凭据
Access-Control-Allow-Origin: https://trusted.com
Access-Control-Allow-Credentials: true
2. Reflection Attack
# [错误] 易受攻击——反射任意 origin
const origin = request.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
# [OK] 正确——白名单校验
const allowedOrigins = ['https://app.com', 'https://admin.com'];
const origin = request.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
3. Subdomain Wildcard
# [错误] 易受攻击——regex 实现不当
const origin = request.headers.origin;
if (/https:\/\/.*\.example\.com/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// 会接受 https://evil.example.com.attacker.com
# [OK] 正确——严格校验
const origin = request.headers.origin;
if (/^https:\/\/[a-z0-9-]+\.example\.com$/.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
按技术划分的安全配置
Node.js/Express (CORS middleware)
const cors = require('cors');
// 安全配置
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
};
app.use(cors(corsOptions));
Nginx
# 条件配置
map $http_origin $cors_origin {
default "";
"~^https://app\\.example\\.com$" $http_origin;
"~^https://admin\\.example\\.com$" $http_origin;
}
server {
location /api {
if ($cors_origin != "") {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
}
if ($request_method = OPTIONS) {
return 204;
}
}
}
Apache
# .htaccess
SetEnvIf Origin "^https://(app|admin)\\.example\\.com$" CORS_ORIGIN=$0
Header always set Access-Control-Allow-Origin "%e" env=CORS_ORIGIN
Header always set Access-Control-Allow-Credentials "true" env=CORS_ORIGIN
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE" env=CORS_ORIGIN
Header always set Access-Control-Allow-Headers "Authorization, Content-Type" env=CORS_ORIGIN
# 响应 OPTIONS 预检请求
RewriteEngine On
RewriteCond % OPTIONS
RewriteRule ^(.*)$ $1 [R=204,L]
Testing CORS
# 使用 curl 测试
curl -H "Origin: https://evil.com" \\
-H "Access-Control-Request-Method: DELETE" \\
-H "Access-Control-Request-Headers: Authorization" \\
-X OPTIONS \\
https://api.example.com/resource
# JavaScript 测试
fetch('https://api.example.com/data', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
}).then(response => console.log(response));
Best Practices
- 切勿在包含敏感数据的 API 中使用通配符 (*)
- 明确的白名单列出允许的源
- 严格校验源,使用安全的 regex
- 尽量减少 credentials:仅在确实必要时启用
- Least privilege(最小权限):仅允许必要的方法和 headers
- Cache preflight(缓存预检):使用 Max-Age 以减少开销
- Monitoring(监控):记录可疑的 CORS 拒绝
安全 CORS 检查清单
- [OK] 源已对照明确的白名单进行校验
- [OK] 校验 regex 不允许绕过
- [OK] 仅在必要时启用 Credentials
- [OK] 方法和 headers 限制到最低限度
- [OK] 预检请求配置正确
- [OK] 已针对恶意源进行测试
- [OK] 监控被拒绝请求的日志
