CORS 跨域重定向后 Origin 变 null —— 一次 Nginx 字体加载失败的排查记录

AI创作声明:以下内容由大模型总结自我与大模型的对话

CORS 跨域重定向后 Origin 变 null ------ 一次 Nginx 字体加载失败的排查记录

现象

博客 blog.letmefly.xyz 页面加载 MathJax 数学公式字体,前端引用地址是 https://letmefly.xyz/Links/JS/MathJax/.../MathJax_Zero.woff。Nginx 会将 letmefly.xyz 的请求 302 重定向到 web.letmefly.xyz。浏览器控制台报出 CORS 错误:

复制代码
Access to font at 'https://web.letmefly.xyz/...' (redirected from 'https://letmefly.xyz/...')
from origin 'https://blog.letmefly.xyz' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

排查过程

第一步:确认 Nginx CORS 配置

Nginx 使用全局 map 变量做 CORS 白名单:

nginx 复制代码
map $http_origin $corsHost {
    default "";
    "~https://blog.letmefly.xyz" https://blog.letmefly.xyz;
    # ...其他域名
}

各 server 块通过 add_header Access-Control-Allow-Origin $corsHost; 输出 CORS 头。看起来没问题。

第二步:curl 验证最终目标

直接带 Origin 请求 web.letmefly.xyz

bash 复制代码
curl -I -H "Origin: https://blog.letmefly.xyz" \
  https://web.letmefly.xyz/Links/JS/MathJax/output/chtml/fonts/woff-v2/MathJax_Zero.woff

返回了 access-control-allow-origin: https://blog.letmefly.xyz,完全正确。

第三步:curl 验证重定向源

bash 复制代码
curl -I -H "Origin: https://blog.letmefly.xyz" \
  https://letmefly.xyz/Links/JS/MathJax/output/chtml/fonts/woff-v2/MathJax_Zero.woff

302 响应也带了正确的 access-control-allow-origin: https://blog.letmefly.xyz

两端 curl 都正确,但浏览器就是报错。这说明浏览器实际发出的请求和 curl 模拟的不一样

第四步:根因定位------Origin 变成了 null

综合以上线索:curl 手动带 Origin 请求没问题,但浏览器报错。说明浏览器跟随 302 重定向后,发到 web.letmefly.xyz 的请求里 Origin 不是 https://blog.letmefly.xyz 。那它变成了什么?答案是字符串 "null"

根因:WHATWG Fetch 规范的 redirect-tainted origin

这不是浏览器 bug,而是 WHATWG Fetch 规范明确要求的行为。

规范定义了 redirect-tainted origin(重定向污染源,旧称 tainted origin flag)机制:

翻译成人话:当一个 CORS 请求经历了跨域重定向(重定向前后的 origin 不同),浏览器会把后续请求的 Origin 头设为字符串 "null"

在这个案例中,请求链是:

复制代码
blog.letmefly.xyz  →(fetch)→  letmefly.xyz  →(302)→  web.letmefly.xyz
     origin                    host A                    host B

letmefly.xyzweb.letmefly.xyz 是跨域重定向,触发 tainted origin flag。浏览器向 web.letmefly.xyz 发送的请求中 Origin: null。Nginx 的 map $http_origin 匹配不到 null,走了 default "",不输出 CORS 头,浏览器拦截。

为什么规范要这样设计? 出于隐私和安全保护。举个例子:假设你在 trusted-bank.com 的页面上,页面向 api.trusted-bank.com 发了一个带 Cookie 的请求。如果 api.trusted-bank.com 被攻击者控制(或攻击者通过 DNS 劫持把请求引到了 evil.com),而浏览器仍然把 Origin: https://trusted-bank.com 原封不动地传过去,那么 evil.com 收到这个请求后就能冒充来自 trusted-bank.com 的合法请求------这就是混淆代理攻击(confused deputy attack)。浏览器把跨域重定向后的 Origin 设为 null,就是为了切断这条信任链:重定向目标不应该自动继承原始请求的可信身份。

各浏览器实现情况

浏览器 行为 参考
Chrome 跨域重定向后 Origin: null Chromium Issue 154967
Firefox 跨域重定向后 Origin: null Bug 1444278(Firefox 后续版本已对齐规范)
Safari 跨域重定向后 Origin: null 同规范行为

所有现代浏览器都遵循这一规范行为。Firefox 的 Bug 1444278 记录了完整的修复过程:之前 Firefox 在跨域重定向后仍然发送原始 Origin(与 Chrome/Safari 不一致),后来修复为发送 "null" 以对齐 WHATWG Fetch 规范。规范维护者 annevk 在该 bug 中确认:「Once we cross origin boundaries the request's origin is supposed to become a unique opaque identifier (which serializes to null).」

解决方案

最佳方案(推荐) :在前端直接引用最终地址 https://web.letmefly.xyz/...,避免经过 letmefly.xyz 的重定向,根本不触发 tainted origin flag。

备选方案一 :在 web.letmefly.xyz 的 Nginx 配置中,对字体文件用 * 通配符:

nginx 复制代码
location ~* \.(woff|woff2|ttf|eot|otf)$ {
    add_header Access-Control-Allow-Origin "*" always;
}

字体文件本身不涉及敏感数据,用 * 通配是安全的。

备选方案二 :在 map 中加一条匹配 null 的规则:

nginx 复制代码
map $http_origin $corsHost {
    default "";
    "null" "https://blog.letmefly.xyz";
    "~https://blog.letmefly.xyz" https://blog.letmefly.xyz;
    # ...
}

但这样做安全性稍差------任何 Origin 为 null 的请求都会被放行为 blog.letmefly.xyz

排查过程中踩的另一个坑:Nginx add_header 继承陷阱

排查过程中还踩了一个坑:在 web.letmefly.xyz 的 server 块级别加了 add_header Access-Control-Allow-Origin $corsHost;,但不生效。

原因涉及 Nginx add_header 的继承机制。根据官方文档的描述:

These directives are inherited from the previous configuration level if and only if there are no add_header directives defined on the current level.

也就是说,add_header 默认是会继承上层的 ,但一旦当前层级(如 location)里出现了任何一条 add_header,上层(server/http)的 add_header全部失效------不是合并,而是完全替换。

web.letmefly.xyz 的配置为例,假设原本有这样的结构:

nginx 复制代码
server {
    server_name web.letmefly.xyz;
    add_header Access-Control-Allow-Origin $corsHost always;  # ← server 级别

    location / {
        # 这个 location 里没有任何 add_header → 会继承 server 级别的 CORS 头 ✓
        try_files $uri $uri/ =404;
    }

    location /api {
        add_header X-Frame-Options SAMEORIGIN;  # ← 一旦出现这条
        # server 级别的 add_header 全部失效,CORS 头丢失 ✗
    }
}

解决办法是把 CORS 头直接加到每个含有 add_header 的 location 块里。可以用 include 抽成公共片段避免重复:

nginx 复制代码
# /etc/nginx/snippets/cors.conf
add_header Access-Control-Allow-Origin $corsHost always;

然后在每个需要 CORS 头的 location 中引入:

nginx 复制代码
server {
    server_name web.letmefly.xyz;
    listen 443 ssl;
    root /srv/web/website;

    location / {
        include /etc/nginx/snippets/cors.conf;
        try_files $uri $uri/ =404;
    }

    location /api {
        include /etc/nginx/snippets/cors.conf;
        add_header X-Frame-Options SAMEORIGIN;
        proxy_pass http://backend;
    }

    location ~* \.(woff|woff2|ttf|eot|otf)$ {
        include /etc/nginx/snippets/cors.conf;
        expires 30d;
    }
}

这样即使某个 location 有自己的 add_header,CORS 头也不会丢失。

补充 :Nginx 1.29.3 新增了 add_header_inherit merge; 指令(1.30.0 stable 已包含),可以让子级别在保留自己 add_header 的同时继承上层的 add_header


写完这篇文章后才发现,最好的排查方式是一开始就在浏览器 DevTools 的 Network 面板里查看重定向后实际发出的请求头......不过绕了一圈学到的东西更多(大概吧)。

同步发文于CSDN和我的个人博客,(AI)创作不易,转载经作者同意后请附上原文链接哦~

千篇源码题解已开源

相关推荐
程序猿阿伟2 小时前
《QClaw隐藏的GitHub自动化神级用法》
运维·自动化·github
ulias2122 小时前
进程初识(1)
linux·运维·服务器·网络·c++
福大大架构师每日一题2 小时前
nginx 1.30.0稳定版深度解析:Early Hints、HTTP/2后端、MPTCP全量上线,1.29.x分支精华全面整合
运维·nginx·http
Paraverse_徐志斌3 小时前
Linux 内核与 Zero-Copy 零拷贝
linux·运维·内核·零拷贝
繢鴻3 小时前
Nginx 配置顺序错误
运维·nginx·阿里云·持续部署
木雷坞3 小时前
2026年4月实测:K8s containerd 镜像拉取全方案汇总
运维·容器·kubernetes
℡終嚸♂6803 小时前
Vite 开发服务器文件读取 Writeup
运维·服务器·状态模式
开开心心_Every3 小时前
动图制作工具,拆分转视频动态照离线免费
运维·前端·人工智能·edge·pdf·散列表·启发式算法
fqrj20263 小时前
什么是WordPress?企业WordPress搭建网站的基本流程和步骤分享
html·wordpress·网站开发·技术开发