CORS 跨域安全配置是 Web 安全基线中高发且隐蔽的一环:误配 Access-Control-Allow-Origin: * 或反射任意 Origin,会让攻击者在任意站点通过 JavaScript 读取受害者的认证态数据,造成账户接管与信息泄露。本文从原理到 Nginx 实战给出安全配置方案。
适用场景
- 前后端分离架构(前端
app.example.com,APIapi.example.com)的跨域接口 - 使用 Nginx 反代 API 网关、需在网关层统一管理 CORS 头的场景
- 渗透测试发现
Access-Control-Allow-Origin配置过宽、需收敛的业务
前置条件
- Nginx 版本 1.16+(本文语法兼容 nginx 1.18 – 1.26)
- 可修改 Nginx 配置并执行
nginx -s reload的权限 - 已确定合法的跨域来源清单(前端域名列表)
原理说明
浏览器同源策略默认禁止跨域读取响应,CORS 是服务器显式「放行」跨域读取的机制,核心是三个响应头:
Access-Control-Allow-Origin(ACAO):声明允许的来源,只能是单一具体 Origin 或 *,不能是变量拼出的「任意反射」。Access-Control-Allow-Credentials:为 true 时允许携带 Cookie,此时 ACAO 绝不能是 *,必须精确匹配来源。Access-Control-Allow-Methods / Headers:声明放行的请求方法与自定义头,配合OPTIONS预检(Preflight)请求使用。
常见漏洞模式:① ACOA 恒为 * 且 Allow-Credentials=true(浏览器实际会拒绝,但很多服务端实现是反射 Origin + 恒 true,等于放开所有来源);② 反射请求 Origin 且不做白名单校验——攻击者在 evil.com 发起带凭据请求,服务端回显该 Origin 并允许读取响应,用户数据即被跨域窃取。
操作步骤
1. 白名单校验 + 精确回显(Nginx 网关层)
# /etc/nginx/conf.d/cors.conf —— 在 server 块中 include 或在 location 内使用
map $http_origin $cors_origin {
default "";
"~^https?://(www\.)?example\.com$" $http_origin;
"~^https?://admin\.example\.com$" $http_origin;
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
# 仅当来源命中白名单时才输出 CORS 头
if ($cors_origin) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
}
# 预检请求直接返回 204
if ($request_method = OPTIONS) {
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, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary Origin always;
return 204;
}
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
关键点:map 白名单用正则精确匹配域名;未命中来源时 $cors_origin 为空串,不输出任何 CORS 头,浏览器自然拦截;Vary: Origin 让 CDN 缓存按来源区分。
2. 服务端二次校验(Java / Python 示例)
# Python Flask 示例:白名单校验后精确回显
from flask import request, make_response
ALLOWED_ORIGINS = {"https://app.example.com", "https://admin.example.com"}
@app.after_request
def set_cors(resp):
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
resp.headers["Access-Control-Allow-Origin"] = origin
resp.headers["Access-Control-Allow-Credentials"] = "true"
resp.headers["Vary"] = "Origin"
return resp
3. 通用请求的兜底策略
- 不涉及 Cookie 的纯 API:可简化为固定白名单 Origin + 不开 Allow-Credentials。
- 仅需同站读取:优先用
SameSiteCookie + 同域反代,从根上消灭跨域面。 - 防止子域被接管后扩大风险:白名单不要写成
~^https?://.*example\.com$这种通配子域形式,evil.example.com若被接管(如 DNS 悬空)同样能跨域读数据。
配置验证
# 1. 白名单来源:应返回精确 Origin
curl -sI -H "Origin: https://app.example.com" https://api.example.com/api/user | grep -i "access-control\|vary"
# 2. 非白名单来源:不应出现 Access-Control-Allow-Origin
curl -sI -H "Origin: https://evil.com" https://api.example.com/api/user | grep -i "access-control" || echo "PASS: 未输出 CORS 头"
# 3. 预检请求验证
curl -sI -X OPTIONS -H "Origin: https://app.example.com" -H "Access-Control-Request-Method: GET" https://api.example.com/api/user | grep -i "allow-methods\|allow-headers\|max-age"
# 4. 带凭据场景确认 ACOA 非 *
curl -sI -H "Origin: https://app.example.com" -H "Cookie: session=test" https://api.example.com/api/user | grep -i "allow-credentials"
也可直接在浏览器控制台用 fetch 验证:
fetch("https://api.example.com/api/user", {credentials: "include"})
.then(r => r.json()).then(console.log)
# 若网络面板报 CORS 错误 → 配置生效中;能读到数据 → 需确认来源是否本就在白名单
常见问题(FAQ)
Q1:为什么配置了 CORS 还是报「Access-Control-Allow-Origin」错误?
常见原因:① 预检 OPTIONS 请求被 Nginx if 拦截前未输出头,需确认 OPTIONS 分支在 proxy_pass 之前且无 return 之外的干扰;② 同时有多个 add_header 来源时,Nginx 只有 always 或同一继承块内才会全部生效——注意 add_header 在非 always 时会被同 location 内其他 add_header 覆盖;③ CDN/WAF 层剥离了自定义头,需在链路各层检查响应头。
Q2:ACOA 可以设置为多个域名吗?
不能。ACOA 只接受单个值或 *。多来源场景必须用「服务端判断 Origin 是否命中白名单 → 回显具体 Origin」的方式(即本文的 map + 精确回显),这是标准且安全的做法。
Q3:如何发现存量 CORS 配置漏洞?
可用 Burp Suite 的 CORS Scanner 扩展或手动测试:修改 Origin 为 evil.com、null、https://example.com.evil.com 三个值分别请求,观察响应头。若任意值被回显且 Allow-Credentials=true,即存在反射型 CORS 漏洞,应纳入整改清单。
总结
CORS 安全的核心就一句话:只对白名单来源精确回显,绝不做任意反射。通过 Nginx map 白名单 + 预检处理 + Vary 头,可在网关层统一收敛全部跨域策略;服务端再叠加二次校验双保险。整改后务必用白名单/非白名单/带凭据三组请求回归验证,确保线上策略符合预期。