作者:闫乾苓

一、背景:CORS 漏洞为何值得关注?

跨域资源共享(CORS, Cross-Origin Resource Sharing)是现代 Web 开发中绕不开的核心机制。它通过放宽浏览器的同源策略(Same-Origin Policy),允许前端应用从一个源(Origin)安全地请求另一个源的资源。然而,若配置不当,CORS 反而会成为数据泄露的入口。
在一次安全评估中,我们发现目标系统存在一个典型的 CORS 配置缺陷:

请求头:Origin: https://www.baidu.com
响应头:Access-Control-Allow-Origin: https://www.baidu.com

这意味着:任意第三方网站(如百度)均可通过前端 JavaScript 跨域读取该接口的敏感数据。虽然该漏洞被定为“低危”,但若结合其他漏洞(如身份认证缺陷),仍可能造成严重后果。

二、漏洞原理简析

2.1 什么是“动态反射 Origin”?

许多开发者为了“方便”,在服务端直接将请求头中的 Origin 值原样写入响应头:

Access-Control-Allow-Origin: <请求中的 Origin>

这种做法看似“灵活”,实则完全放弃了访问控制——任何网站都能跨域访问你的 API。

2.2 为什么不能用 *?

虽然 Access-Control-Allow-Origin: * 可以允许所有源访问,但它禁止携带凭证(如 Cookie、Authorization)。一旦你的 API 需要身份认证,就必须指定具体源,而不能使用通配符。

三、安全修复方案:基于 Nginx 的白名单机制

我们采用 Nginx + 白名单映射 的方式,实现安全、高效、可维护的 CORS 策略。

3.1 核心思路

  • 拒绝动态反射:绝不直接回显 Origin。
  • 启用白名单:仅允许预定义的可信源跨域访问。
  • 区分预检与实际请求:正确处理 OPTIONS 请求。

3.2 配置步骤详解

步骤 1:创建白名单映射文件

创建 /etc/nginx/conf.d/cors_whitelist.conf:

map $http_origin $cors_origin {
    default "";
    # 可信源列表(必须完整、精确)
    "https://www.yourdomain.com"     "https://www.yourdomain.com";
    "https://app.yourdomain.com"     "https://app.yourdomain.com";
    "http://192.168.61.11"           "http://192.168.61.11";      # 开发环境 IP
    "http://192.168.61.11:8080"      "http://192.168.61.11:8080"; # 带端口的前端
}

关键点:

  • 白名单必须包含协议 + 域名/IP + 端口(非默认端口时)
  • default “” 表示不在白名单中的 Origin 不返回 Access-Control-Allow-Origin 头

步骤 2:创建通用 CORS 响应头配置

创建 /etc/nginx/conf.d/cors.conf:

add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,Content-Type,Authorization" always;
# add_header Access-Control-Allow-Credentials "true" always;  #若 API 需要携带 Cookie / Token, 按需启用

注意:

  1. always 参数确保即使返回 4xx/5xx 也包含 CORS 头,避免前端调试困难。
  2. 若 API 需要携带 Cookie / Token,必须设置 Access-Control-Allow-Credentials: true,并在白名单场景下加上 add_header Access-Control-Allow-Credentials “true” always; “启用Access-Control-Allow-Credentials: true时,前端请求需设置withCredentials: true(XMLHttpRequest/fetch)”,避免因前后端配置不匹配导致凭证携带失败。

步骤 3:在 Server 块中集成配置

server {
    listen 443 ssl;
    server_name xxxx.xxxx.com.cn;
location /xxx/api/xxx {
   # 拒绝非白名单 Origin
     if ($http_origin != "" && $cors_origin = "") {
         return 403;
     }
        # 处理预检请求(OPTIONS)
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin $cors_origin;
            add_header Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE";
            add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,Content-Type,Authorization";
			# add_header Access-Control-Allow-Credentials "true"; # #若 API 需要携带 Cookie / Token, 按需启用
            add_header Access-Control-Max-Age 1728000;  # 缓存 20 天
            add_header Content-Length 0;
            return 204;  # 必须返回 204,无响应体
        }
        # 加载通用 CORS 头(用于实际请求)
        include conf.d/cors.conf;
        proxy_pass http://backend_server;
    }
}

四、技术细节:为什么这样配置?

4.1 预检请求(Preflight)处理

当请求满足以下任一条件时,浏览器会先发 OPTIONS 预检请求:

  • 使用 PUT、DELETE 等非简单方法
  • 设置自定义头(如 Authorization)
  • Content-Type 为 application/json
    Nginx 必须在 OPTIONS 响应中明确允许这些操作,否则浏览器会直接拦截后续请求。

4.2 Access-Control-Max-Age 的作用

设置 Access-Control-Max-Age: 1728000(20 天)可让浏览器缓存预检结果,避免每次请求都发 OPTIONS,显著提升性能。

开发阶段建议设为 10 秒,便于调试。

4.3 为何 return 204?

  • 204 No Content 表示“成功但无响应体”
  • 必须避免返回 HTML 或 JSON,否则浏览器可能认为预检失败
  • return 204 会立即终止请求处理,跳过后续 proxy_pass 等逻辑

五、测试验证:确保修复有效

5.1 验证白名单内源

curl -H “Origin: https://app.yourdomain.com” -I https://xxxx.xxxx.com.cn:443/xxx/api/xxx
预期响应头:
Access-Control-Allow-Origin: https://app.yourdomain.com

5.2 验证白名单外源

curl -H “Origin: https://www.baidu.com” -I https://xxxx.xxxx.com.cn:443/xxx/api/xxx
预期结果:无 Access-Control-Allow-Origin 头
→ 浏览器将拦截响应,前端无法读取数据。

5.3 验证 OPTIONS 预检

curl -X OPTIONS \
     -H "Origin: https://app.yourdomain.com" \
     -H "Access-Control-Request-Method: POST" \
     -H "Access-Control-Request-Headers: Authorization" \
     -I https://xxxx.xxxx.com.cn:443/xxx/api/xxx

预期:返回 204,且包含完整的 Allow-Origin、Allow-Methods、Allow-Headers。

六、上线建议

  • 先在测试环境验证:确保所有业务前端域名/IP 都已加入白名单。
  • 申请割接窗口:避免因遗漏白名单导致线上业务中断。
  • 监控日志:上线后观察是否有大量 OPTIONS 请求或 CORS 错误。

七、总结

CORS 本身不是漏洞,错误的配置才是。通过 Nginx 的 map 模块实现白名单机制,既能满足业务跨域需求,又能有效防范未授权访问。安全无小事,即使是“低危”漏洞,也值得我们以严谨的态度对待。
安全原则:永远不要信任客户端输入,包括 Origin 头。
附:参考配置文件结构

/etc/nginx/
├── nginx.conf
└── conf.d/
    ├── cors_whitelist.conf   # 白名单映射
    └── cors.conf             # CORS 响应头

通过模块化配置,未来增删可信源只需修改 cors_whitelist.conf,无需改动主逻辑,提升可维护性与安全性。