Web 的 CORS:跨源资源共享的横纵分析

发布于 · 6,935 字 · 约 17 分钟#安全
Web 的 CORS:跨源资源共享的横纵分析 封面图
  • CORS 的本质是服务器通过 HTTP 响应头显式声明允许哪些源站脚本读取资源,解决的是跨源读取而非跨源请求发送
  • 预检请求(OPTIONS)主要是兼容性机制而非安全特性,其边界刻意对齐 HTML 表单的历史能力,保护的是从未预期跨源非简单请求的旧服务器
  • CORS 规范从 2004 年提案到 2014 年成为 W3C 推荐标准历经十年,2020 年被 Fetch 标准吸收取代;期间 JSONP、window.name、片段标识符等补丁方案各留安全坑
  • IE8/9 曾以私有对象 XDomainRequest 走独立跨源路线,仅支持匿名 GET/POST,造成两套跨源 API 并存,直到 IE10 才对齐标准
  • 跨域方案应区分资源读取与通信两个维度:CORS、代理属前者,postMessage、WebSocket 属后者;WebSocket 不受 CORS 预检约束,依赖握手 Origin 头做安全校验

一、一句话定义

CORS(Cross-Origin Resource Sharing,跨源资源共享)是浏览器同源策略(SOP)的一个“受控例外”机制:它用一组 HTTP 响应头,让服务器显式声明“允许哪些源站的脚本读取本资源”,在守住 SOP 安全模型的前提下,给跨源 fetch / XMLHttpRequest 开一道可审计的口子。

它解决的是 “脚本能不能读跨源响应”,不是“能不能发请求”。这个区别,是整个故事里最容易被忽视、也最致命的一个点。

二、纵向分析:从同源铁壁到受控开口

2.1 同源策略的诞生:一道为 DOM 而设的墙(1995)

一切要从一个反直觉的事实说起:同源策略出现的时候,浏览器里根本没有 XMLHttpRequest。

1995 年,Netscape Navigator 2.0 带着 JavaScript 登场。脚本一旦能操作 DOM,“跨站脚本读另一个站点的文档内容”就成了现实威胁。Netscape 的解法是给脚本能力套上“最小权限隔离”的默认策略:同源策略(SOP)。Netscape 官方客户端 JS 指南里写得很清楚:来自不同 origin 的脚本,不能 get/set 另一个窗口或 frame 里特定浏览器与 HTML 对象的属性。受保护的清单里排在最前的,是 document.cookie、document.domain、document.URL、location。

这里有个关键的历史细节:SOP 最初守护的是 “读”:跨源读取响应和 DOM。1995 年还没有 XHR,根本谈不上“发请求”。这就埋下了一个延续至今的矛盾:SOP 默认“写允许、读禁止”。后来的 CSRF(跨站请求伪造)问题,恰恰是从“写被默许”这个原点长出来的。

至于 cookie,它有一套独立的按 domain/path 作用域的规则,还能跨子域共享,并不是由 JS 层的 SOP 直接保护。严格说,SOP 最初保护的核心是 DOM 和响应读取,document.cookie 只是被列为“受源检查约束的属性”之一。

2.2 第一次松动:document.domain 与签名脚本(1996-1997)

纯粹的 SOP 太死板了。同一家公司,store.company.com 和 www.company.com 居然无法协作。Netscape Navigator 3(1996)于是引入 document.domain:脚本可以把自己的域“收窄”到共同父域,双方都设成 company.com 后就能互访。Navigator 4(1997)又用签名脚本(signed script)走“显式授权提权”的路线,靠 PrivilegeManager.enablePrivilege 申请权限。

这两次尝试都是在“隔离”与“协作”之间找平衡,但都因为复杂、易错、不安全而被后来者取代。今天 document.domain 已经被 MDN 标为 deprecated:它隐式把端口置为 null,还绕开了 localStorage、IndexedDB 等一大批 Web API 的源检查。一个补丁,最终成了技术债。

2.3 AJAX 引爆跨域需求(1999-2004)

1999 年,IE5 引入 XMLHttpRequest(后标准化为 XHR),AJAX 时代拉开帷幕。前端突然有了“用脚本读后端数据”的欲望,可 SOP 依旧禁止 XHR 跨源。需求与现实之间裂开一道大口子。

2004 年 3 月,Tellme Networks 的 Matt Oshry、Brad Porter、Michael Bodell 提出跨源支持方案,这是 CORS 思想可追溯的最早提案之一。但标准不会一夜成形,开发者等不及,先自己动手。

2.4 CORS 之前的野路子:JSONP 与种种补丁(2004-2009)

在官方机制缺位的五年里,开发者把浏览器里“不受 SOP 约束的特性”全利用了一遍,凑出一堆补丁:

  • JSONP:George Jempty 在 2005 年 7 月提出雏形,Bob Ippolito 在 2005 年 12 月 5 日正式定型:用 <script> 标签的回调参数包裹 JSON(foo({...}))。<script src> 不受 SOP 限制,服务器把数据当 JS 函数调用返回,浏览器执行后数据“推”回调用方。它解决了“无后端代理跨域取数”,迅速被 Dojo、GWT 采用。但它的本质是一段友善的跨站脚本:响应在调用方的安全上下文里执行,调用方必须完全信任远程服务器;没有错误状态码、难处理非 200、容易被注入。它只支持 GET,只能读不能写。
  • iframe + window.name:利用 window.name 在导航间持久保留字符串的特性,跨源 frame 把数据写进自己的 window.name,再跳回与父页同源的 URL,父页即可读取。流程脆弱,属于“黑客式”通道。
  • 片段标识符(#):父页创建指向跨源页面的 iframe,目标页把数据写进 parent.location.hash,父页轮询自己的 hash 读取。数据暴露在 URL 里,容量受限,体验差。
  • window.postMessage 早期形态:2007 年 10 月的 WHATWG HTML5 草案已有“Cross-document messaging”章节,当时 postMessage 是单参、用 .domain 校验;到 2010-2011 年的 Web Messaging 草案才演进为 postMessage(message, targetOrigin) 并以 .origin 校验。它能在窗口/iframe 间安全传消息、绕开 SOP,却又不造成跨站脚本,这是和 JSONP 本质不同的另一条路。

每一种补丁都只解决部分场景,又都留下安全或工程上的坑。正是这些坑,催生了“一种由服务器显式参与的、统一的、安全的跨源读访问机制”,即 CORS。

2.5 规范落地:十年长跑(2005-2014)

CORS 的规范谱系,是一条从“XML 处理指令”一步步长成 HTTP 头方案的路:

  • 2005-06-13:W3C 发布 Note《Authorizing Read Access to XML Content Using the Processing Instruction 1.0》,这是谱系最早公开文档。
  • 2006-2008:工作在 W3C 的 Web Application Formats WG(WAF,后改组为 WebApps WG)推进,规范名为“Access Control for Cross-Site Requests”。核心设计约在 2006 年中稳定,编辑是 Anne van Kesteren(Opera Software);2008 年的需求文档由 David Orchard(BEA Systems) 起草。
  • 2009 年初:规范重命名为“Cross-Origin Resource Sharing”(CORS),统一了 cross-site / cross-domain / cross-origin 的术语混乱,明确了头部解析与预检行为。
  • 2009-03-17:首个以“CORS”命名的 W3C Working Draft 发布,模型已成型:Origin 请求头、Access-Control-Allow-Origin 响应头、简单方法 GET/HEAD/POST、OPTIONS 预检。
  • 2009-10 → 2010-07 → 2012-04(Last Call)→ 2013-01(CR)→ 2013-12(PR)→ 2014-01-16:正式成为 W3C Recommendation。
  • 2020-06-02:W3C 将该规范标记为 Retired,由 Fetch 规范接替。

从 2004 年的一份提案,到 2014 年成为 REC,整整十年。这十年里反复打磨的,是跨源读访问背后那张真实的安全网:究竟该信任什么、该让服务器掌控什么。

2.6 预检请求:被误解的安全机制,实为兼容锁

CORS 区分“简单请求”与“需预检请求”。简单请求(GET/HEAD/POST 且头与内容类型落在安全清单内)浏览器直接发,只带 Origin,由服务器用 Access-Control-Allow-Origin 决定要不要把响应交给脚本。非简单请求(PUT/DELETE、自定义头、非表单 Content-Type)则先发一个 OPTIONS 预检,服务器回 Access-Control-Allow-Methods / Access-Control-Allow-Headers 等“许可”后,真实请求才发出。

但预检主要不是安全特性,而是兼容性机制。Web 上存在海量“假设自己永远不会收到跨源非简单请求”的遗留服务器:它们靠“只有非浏览器客户端才能设置的特定头”来保护自己,或假设永远不会收到跨源 DELETE。CORS 不能让这些端点一夜之间变脆弱。

简单请求的边界,刻意对齐了 HTML <form> 早就能做的跨源提交(GET 与特定 MIME 的 POST)。这些端点本来就活在“需要防 CSRF”的世界里,CORS 只是新增了“由服务器决定是否把响应读给脚本”,并不会比表单提交更危险,于是放行直发,免了预检。凡是超出表单历史能力的请求,都可能触碰从未预期会收到跨源非简单请求的旧服务器,于是先 OPTIONS 预检,让服务器显式表态。

现代 WHATWG Fetch 规范沿用同一逻辑,只是把“simple request”这个旧词换成了“cors-safelisted method/header”和“unsafe-request flag”。

我的判断:预检是 CORS 最被低估、也最被误读的一环。开发者把它当“安全检查”去 Debug,结果卡在 OPTIONS 上几个小时:它其实保护的是“旧服务器”,不是“你的数据”。

2.7 浏览器支持的多轨演义(2009-2012)

  • Firefox 3.5(2009-06-30)和 Safari 4(2009-06-08)首批实现跨站 XHR,withCredentials 同期加入(默认关)。
  • Chrome 3/4(2009)随 WebKit 系跟上。
  • Opera 12 也支持。
  • IE 8/9(2009 / 2011) 只通过私有对象 XDomainRequest(XDR)做有限跨源;直到 IE 10(2012)才在标准 XHR 上支持完整 CORS。IE6/7 则完全没有跨源请求能力。

2.8 IE 的 XDR:一条提前转弯的私有路线

这是 CORS 历史上最值得玩味的分歧点。IE8 阶段,微软团队认为更宽泛的 CORS 提案“攻击面过大”,存在 DNS rebinding、Flash 式 TOCTOU 竞态等未决安全问题,于是另起炉灶做了更“克制、偏安全”的 XDR。

XDR 与标准 CORS 的关键差异:仅匿名请求(不发 cookie)、仅 GET/POST、不允许自定义头、用私有握手头 XDomainRequest: 1 / XDomainRequestAllowed: 1(而非标准 Access-Control-Allow-Origin)、跨安全区域受限、非 W3C 标准。代价是 Web 上出现了“两套跨源 API”并存的局面,开发者必须写 'withCredentials' in xhr 之类的特性检测做降级。直到 IE10 对齐标准、IE 退役,这套私有遗产才退出历史。“IE 到底支持哪一种”成了早期跨源兼容代码挥之不去的幽灵。

2.9 归一:Fetch 标准吞下 CORS(2013-2015)

2013 年 5 月,Anne van Kesteren 发布博客《Fetching URLs》,号召评审 Fetch Standard。背景是:HTML 的 fetch 算法、CORS、CSP 三者定义纠缠、各自为政。Fetch 标准把“跨源取资源”统一成一套模型,吸收(supersede)了 HTML fetch 与 CORS 协议,并整合了重定向、CSP、Service Worker、mixed content 的语义。

Fetch 把 CORS 作为一等公民纳入,定义了清晰的维度:

  • Request mode:same-origin(非同源即报错)、cors(强制走 CORS)、no-cors(默认,只放行 safelisted 方法/头,响应是不透明响应 opaque)、navigate、websocket、webtransport。
  • Credentials mode:omit / same-origin(默认,仅同源带)/ include(总是带)。
  • 关键细节:预检请求本身总是以 same-origin 凭据模式发出(不带凭据),即使真实请求用 include;当 credentials: 'include' 时,Access-Control-Allow-Origin 不能为 *,必须具名且需 Access-Control-Allow-Credentials: true。

把“如何取一个 URL”从 XHR、@font-face、background-image 各自为政的特性里抽离成统一算法,既避免了每处重复且冲突地实现 CORS/CSP/重定向,也让 Service Worker 等新特性得以复用。

2.10 Origin 的正式化与 HTTPS 时代的缠绕(2011-2020)

  • RFC 6454《The Web Origin Concept》(2011-12,Adam Barth 执笔)在 IETF 层面正式定义了 origin 的判定(scheme/host/port 三者相同方为同源)与 Origin 头字段(“null” 用于隐私敏感上下文)。这是整个 Web 安全模型统一的“源”基元,也是 CORS 能跨规范通用的基石。
  • Secure Contexts / Mixed Content(2014-2015,Mike West、Yan Zhu 编辑):定义“potentially secure origin”(https/wss/about),把强力特性限制在安全上下文里。
  • SameSite 默认化(2019-2020):浏览器把 cookie 默认改为 SameSite=Lax;需要跨站携带的 cookie 须显式 SameSite=None 且必须 Secure(仅 HTTPS)。
  • 第三方 cookie 退场(Chrome Privacy Sandbox 等):推动 CHIPS(分区 cookie)、Storage Access API、Related Website Sets 等替代方案。

CORS 管的是“跨源读取响应”,cookie 跨站发送由 SameSite 与“安全上下文”管。MDN 明确:除非请求带凭据,否则浏览器忽略响应里的 Set-Cookie;且 Set-Cookie 是前端 JS 读不到的禁止读取头。第三方 cookie 退场,让“靠 cookie 做跨源认证”的传统模式式微,CORS + token(如 Bearer)与分区存储成为主流。

三、横向分析:跨域方案图谱

3.1 先定坐标:CORS 不是唯一解

CORS 常被当成“跨域=配 CORS”的同义词,但横向看,解决“浏览器里跨源问题”至少有六条路,它们分属两个不同维度:“资源读取”维度(CORS、JSONP、代理、反代)和 “通信”维度(postMessage、WebSocket)。把维度搞混,是大多数混乱的根源。

3.2 六个方案的真实活法

CORS:现代浏览器下跨源资源读取的事实标准。它活成了“服务器显式 opt-in 的授权层”:几乎所有前后端分离项目、开放 API 都用它。前端侵入性低(正常 fetch 即可,只有带凭证时要 credentials: 'include')。它最大的痛点是错误不透明:出于安全,浏览器故意不向 JS 暴露错误细节,前端只拿到笼统失败,具体原因只能去 DevTools Console 看。预检失败、缺头、网络层失败都表现为同一个报错。调试高度依赖服务端改动,服务器说了算。

JSONP:基本已被淘汰。GET-only 的原罪让它无法 POST/PUT/DELETE、无法设自定义头、请求体受 URL 长度限制。它本质是“在调用方源内执行远程返回的 JS”,一旦远程不可信就是反射 XSS。CORS 在 ~2014 获得广泛支持后,JSONP 的存在理由崩塌。但 2021 年的研究(Lekies et al. “JSONPS”)发现 Alexa Top 10,000 里仍有至少 10% 依赖 JSONP:老浏览器兼容、遗留 API、广告像素类注入还在撑着它。新代码一律不该用。

window.postMessage:解决的是“通信”而非“资源读取”,这是它与 CORS 的根本边界。CORS 解决“我的脚本能不能读你服务器返回的数据”;postMessage 解决“两个不同源的窗口/iframe/Worker 之间能不能互相发消息”。它被用来嵌入第三方 iframe(支付、地图、评论)、窗口间协调(OAuth 弹窗回调)、微前端通信。真实混淆点在于:想“读数据”却误用 postMessage(做不到),或想“通信”却去折腾 CORS(多此一举)。它的安全契约是双向的:发送方必须指定精确 targetOrigin(绝不用 *),接收方必须校验 event.origin。

WebSocket:与 CORS 的关系是个高频误解点:WS 不受 CORS 同等方式约束。它用一次 HTTP Upgrade 握手(101 Switching Protocols),之后连接不再是普通 HTTP,所以“预检 OPTIONS + Access-Control-Allow-* 头”那套不适用。替代的安全信号是握手请求里的 Origin 头。RFC 6455 规定服务器可以用 Origin 判断是否接受连接,但这是 MAY(可选)而非 MUST。一个不校验 Origin 的 WS 服务器会接受任意源连接,暴露于跨站 WebSocket 劫持。责任归属因此与 CORS 完全不同:CORS 是“浏览器强制”,WS 校验“责任完全在服务器”。

服务端代理(Node / Webpack devServer / Vite proxy / BFF 转发):核心思路是让请求“先到我控制的同源服务器,再转发到目标”,浏览器看到的是同源请求,根本不触发 SOP。开发期 Vite 的 server.proxy、Webpack 的 devServer.proxy 基于 http-proxy,支持 rewrite/changeOrigin/ws:true/secure:false,让本地前端免配 CORS。生产期则是 BFF(Backend for Frontend)层:把聚合、鉴权、重试放在服务器之间,浏览器只跟同源 BFF 说话。企业偏爱这一支的深层原因,是把 token 留在服务器、只给浏览器一个 HttpOnly SameSite cookie:这是“绕开 CORS”里含金量最高的一支。

反向代理(nginx 反向代理、Cloudflare/Vercel 边缘函数):核心思路是“让浏览器以为前后端同源”。典型 nginx 配置:前端 location / 代理到 localhost:3000,API location /api 代理到 localhost:3030,二者对外都是 http://localhost。注意端口也是 origin 一部分(localhost:3000 与 localhost:3030 是不同源),这种情况下通常完全不需要 CORS 头。常见坑:在 nginx 和后端都设 CORS 头会出现"Access-Control-Allow-Origin 不匹配 *, *"重复头错误,只能在一层设;nginx 的 301 重定向会丢 CORS 头,改用 307。

六个方案对比一览:

维度CORSJSONPpostMessageWebSocket服务端代理 / BFF反向代理 / nginx
解决的本质资源读取授权资源读取(历史补丁)窗口间通信全双工通道同源中转绕开策略伪装同源
安全模型浏览器强制、服务器显式 opt-in执行远程 JS(脆弱)双向 origin 校验服务器应校验 Origin(非强制)同源内、token 不出服务器同源即无策略
凭证支持支持(有铁律)默认带 cookie(XSSI 风险)N/A(传消息)握手后可带 cookie最佳(HttpOnly cookie)同 CORS 或更佳
前端侵入性低低但危险中(需收发逻辑)中无(透明)无(透明)
错误调试难度高(不透明)低但无回调中(需自己校验)中(靠服务器日志)低(普通网络错误)低
为何被选中现代标准、通用仅遗留兼容跨窗口/iframe 通信实时双向安全 + 免 CORS彻底消除跨域

3.3 框架与服务器里的 CORS 实践

Node / Express:用 cors 包。origin 接受 boolean(true=反射请求 origin、false=禁用)、string、RegExp、Array、Function(动态判定);credentials: true 时 origin 不能为 *;用函数可逐请求生成配置(如 /auth/* 用具体源+凭证,其余 *)。常见误配:把 origin 写死成单一源导致多前端/多环境失败;credentials: true 配 origin: '*'(浏览器直接报错)。

Spring Boot:全局实现 WebMvcConfigurer.addCorsMappings,按 URL pattern 配 allowedOrigins/allowedMethods/allowedHeaders/exposedHeaders/allowCredentials/maxAge;方法/类级用 @CrossOrigin 做细粒度控制。allowCredentials(true) 时 allowedOrigins 必须是具体域名。allowCredentials 默认关。

Django / Flask / FastAPI:Django 无内置 CORS 中间件,依赖第三方 django-cors-headers(CORS_ALLOWED_ORIGINS、CORS_ALLOW_CREDENTIALS)。Flask 用 flask-cors,CORS(app, resources={r"/api/": {"origins":"http://localhost:8000","supports_credentials":True}}),配置优先级:资源级 > 关键字参数 > app config > 默认;supports_credentials=True 不能配 "*" origin。FastAPI 内置 fastapi.middleware.cors.CORSMiddleware(allow_origins、allow_origin_regex、allow_credentials、allow_private_network 等),中间件自动处理 OPTIONS 预检与简单请求两类。

nginx:常见套路在 location 内 add_header 'Access-Control-Allow-Origin' ...,并用独立 if ($request_method = 'OPTIONS') 块直接 return 204 处理预检。关键坑:nginx if 块创建新上下文,add_header 不继承父级,OPTIONS 块内要重复写 CORS 头;加 always 参数确保 401/403/404/500 也带 CORS 头;上游已发 CORS 头时用 proxy_hide_header 剥掉避免重复。多前端用 map 基于 $http_origin 做白名单,记得加 Vary: Origin。

云 / 边缘:AWS API Gateway:REST API 需手动建 OPTIONS mock 集成并设响应头(控制台“Enable CORS”会自动加,但必须 redeploy);HTTP API 更省心,配了 CORS 后网关自动响应预检 OPTIONS。Vercel:Functions 默认不加 CORS 头,静态配置写 vercel.json 的 headers(全局推到 CDN),动态反射用 Routing Middleware。Cloudflare:用 Workers / Snippets / Transform Rules 设 CORS,规则按顺序执行、后者可覆盖前者。Supabase Edge Functions:默认 Access-Control-Allow-Origin: '*',必须显式处理 OPTIONS 预检,限制源需把 '*' 改成具体 origin 字符串(无独立配置面板,在代码内控制)。

3.4 企业级:网关与 BFF 的统一收敛

企业偏爱把 CORS 当横切关注点,在网关层(Kong、AWS API Gateway)或 BFF 层统一加 Access-Control-Allow-* 头,后端服务不再各自实现 CORS 逻辑。Kong 用插件式 KongPlugin(声明 origins/methods/headers/credentials/max_age)挂到 route/service。

BFF(Backend for Frontend)模式更深一层:为每种客户端各建一个轻量网关,并行扇出到多个后端、聚合响应。企业选它绕开 CORS 的真实理由有五条:token 不出服务器(BFF 持有 client secret,浏览器只拿 HttpOnly+Secure+SameSite 会话 cookie,XSS 偷不到)、消除 CORS 复杂性(UI 与 BFF 同源)、缩小公开攻击面、服务端会话可控(强制登出、吊销 token)、前端团队自治。但“只把 bearer token 透传给浏览器的反向代理”不是真 BFF,真 BFF 的判定特征是“token 永不出服务器”。代价是 BFF 成为扩缩容热点、有运维开销;对无敏感数据、XSS 防御强的纯公开应用,配置良好的 PKCE SPA 可能更划算。

四、横纵交汇洞察

4.1 历史如何塑造今天的竞争位置

把纵向长河与横向版图叠在一起看,CORS 今天的地位是三段历史合力锁定的:

第一,SOP 诞生于“保护 DOM 读取”、且早于 XHR,这决定了它天然是“默认禁止读、默许写”:CORS 因此只能是一个“读”的例外,而非“写”的例外。这解释了为什么 CORS 管不住 CSRF:CSRF 是“写”,从根上就不在 CORS 的射程内。

第二,预检被设计成“兼容锁”而非“安全锁”,这决定了今天无数遗留服务器能“无感”接入 CORS,但也决定了 CORS 的调试黑洞:它把复杂性推给了开发者的 DevTools,而不是给出可读错误。

第三,IE 的 XDR 私有路线虽已退役,但它留下的“双轨认知”让一代前端在 'withCredentials' in xhr 式的降级代码里耗费青春,也间接延缓了“跨源=标准 CORS”的心智统一。

4.2 优势与劣势的历史根源

CORS 今天的每个核心优势,都能在历史上找到锚点:

  • “统一、标准、浏览器强制” → 源自 2006-2014 十年规范长跑,以及 2013 年 Fetch 把它收编为一等公民,从此跨 XHR/@font-face/Service Worker 共用一套模型。
  • “服务器完全掌控授权” → 源自 SOP 的 opt-in 哲学:CORS 只是“服务器授权浏览器把响应读给脚本”,不是访问控制。

每个核心劣势,也能追溯到一个历史决策:

  • “错误不透明、调试地狱” → 源自安全优先的设计(JS 不得见错误细节)+ 预检的兼容定位。当初的好决策(保护错误细节),成了今天的包袱。
  • "凭证请求 * 铁律导致的误配高发“ → 源自”简单请求对齐 HTML form"的兼容妥协:当 credentials: 'include' 撞上通配符,规范只能一刀切禁止,留下“反射 Origin”这条危险捷径。

4.3 安全:CORS 最大的暗面

这是横向里最该被单独拎出来的判断:CORS 不是安全层,它是授权层;CORS 不是 CSRF 防护。

最典型的致命误配是反射 Origin:服务器不校验,直接把客户端发来的 Origin 原样写回 Access-Control-Allow-Origin。一旦同时返回 Access-Control-Allow-Credentials: true,任意网站都能以受害者身份发凭证请求并读取完整响应。PortSwigger 披露过一起真实案例:对一家比特币交易所反射了 https://fiddle.jshell.net 并带凭证,攻击者用 XHR 盗走用户私钥 API key,进而转走资金,披露后 20 分钟内修复。

其他高频暗面:通配符 * 与 Access-Control-Allow-Credentials: true 的危险组合(规范禁止,但开发者常把 * 改成“反射 Origin”来“修复”,功能上等价于 *+凭证,反而更糟);null origin 被信任(sandbox iframe 可强制 Origin: null 带凭证读响应);信任整个父域/任意子域(任一子域 XSS 即可读父域凭证响应)。OWASP 把 CORS 错误配置归入 A05:2021(Security Misconfiguration),关联 CWE-942(Permissive Cross-domain Policy with Untrusted Domains)。

真实漏洞已在野外:CVE-2025-34291(Langflow,CVSS 9.4)因 Access-Control-Allow-Credentials: true 对任意源 + SameSite=None; Secure 的 refresh_token cookie,使恶意页跨源盗 token 后调 /api/v1/run 实现 RCE,已被列入 CISA KEV、由 MuddyWater 组织野外利用;CVE-2025-50579(Nginx Proxy Manager)因反射恶意 Origin 可窃取 JWT 实现账户接管;GHSA-hqm9-5xxw-4qxp(Grav CMS)每响应 Access-Control-Allow-Origin: * 且 token 经 URL 传递,可建超级管理员。

社区痛点是这套机制的“人因”证据:Stack Overflow 上 “No 'Access-Control-Allow-Origin' header” 的最高赞答案反复强调“CORS 是服务端许可系统,你控制不了别人的服务器”;HN 上有开发者直言“Even the HN comments here are a sea of confusion and contradiction”(连评论区都是一片困惑与矛盾)。最讽刺的一个案例:有用户在 Firefox 下因跟踪保护拦截了 www.reddit.com 的 XHR,报错却表现为 CORS 错误,耗了大量时间排查自己教程里的代码,CORS 的“不透明”,连错误归因都污染了。

4.4 三个剧本

最可能的剧本(基线):CORS 作为“资源读取授权层”继续存在,地位稳固。开发期靠 Vite/Webpack 代理、生产期靠 BFF/网关收敛,成为前后端分离架构的默认组合。预检与 *+凭证铁律依旧是高频踩坑点,但 AI 辅助编码会把正确的 CORS 配置样板更快推给新手,整体误配率缓慢下降。

最危险的剧本:第三方 cookie 退场 + 隐私沙箱推进,让“靠 cookie 做跨源认证”加速式微,更多团队被迫改用 token 模式;而 token 模式下,一旦把 Access-Control-Allow-Origin 反射或宽配 + credentials,攻击面比 cookie 时代更大(token 无 HttpOnly 护体)。若社区仍把 CORS 当“开关”而非“授权层”,CVE-2025-34291 式的跨源 RCE 会更多。

最乐观的剧本:Private Network Access(原 CORS-RFC1918)强化对私有网络的预检、CHIPS/Storage Access API 解决分区存储、Fetch 模型进一步统一,CORS 从“需要逐个端点手配的脆弱开关”演进为“由框架与边缘平台默认安全收敛的底座”。那时开发者几乎不再手写 CORS 头,遗留误配随旧框架淘汰而自然消亡。

收尾的回环:1995 年那道为 DOM 而设的墙,初衷是“别让脚本读别人的文档”。三十年后,我们仍在用一组 HTTP 头,小心翼翼地给那道墙开一个个受控的窗:开多大、给谁开,仍是每个端点要自己回答的问题。

五、信息来源

以下来源均于 2026-07-20 访问。标注【一手】= 规范/官方文档/官方博客;【权威二手】= 百科/厂商归档。

纵向(SOP 与 CORS 历史)

  1. Same-origin policy · Wikipedia(SOP 由 Netscape Navigator 2.02/1995 引入、document.domain 由 Nav 3/1996 引入)【权威二手】https://en.wikipedia.org/wiki/Same-origin_policy
  2. Netscape Client-Side JS Guide(Oracle 归档,含 SOP 定义、data tainting、signed scripts)【一手】https://docs.oracle.com/cd/E19957-01/816-6409-10/sec.htm
  3. JSONP · Wikipedia(Jempty 2005-07 雏形、Ippolito 2005-12-05 提案)【权威二手】https://en.wikipedia.org/wiki/JSONP
  4. Cross-document messaging · Wikipedia;WHATWG HTML Web Messaging【一手】https://html.spec.whatwg.org/multipage/web-messaging.html
  5. CORS 规范完整时间线(Note 2005-06-13 → WD 2009-03-17 → REC 2014-01-16 → Retired 2020-06-02)· W3C【一手】https://www.w3.org/standards/history/cors/
  6. 首个 CORS 命名 WD(WD-cors-20090317,Anne van Kesteren 编辑)【一手】https://www.w3.org/TR/2009/WD-cors-20090317/
  7. CORS 起源:Tellme Networks 2004 提案、WAF/WebApps WG、Anne van Kesteren 编辑、David Orchard 需求文档 · Wikipedia CORS【权威二手】https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
  8. IE XDomainRequest(IE8/2009 私有实现)· MSDN Magazine 2009 / MS-CORS【一手】https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/march/internet-explorer-8-new-features-to-slice-store-and-accelerate-your-web-applications
  9. Fetch 标准吸收 CORS(mode / credentials 设计)· WHATWG Fetch Standard【一手】https://fetch.spec.whatwg.org/
  10. Anne van Kesteren《Fetching URLs》(2013-05,宣布 Fetch 标准)【一手】https://annevankesteren.nl/2013/05/fetching-urls
  11. RFC 6454《The Web Origin Concept》(2011-12,Adam Barth)【一手】https://www.rfc-editor.org/rfc/rfc6454
  12. Secure Contexts / Mixed Content(2014-2015,Mike West、Yan Zhu)· W3C【一手】https://www.w3.org/TR/2015/CR-mixed-content-20150317/
  13. Set-Cookie / SameSite、第三方 cookie 退场 · MDN【一手】https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie

横向(方案对比与框架实践)

  1. MDN CORS 指南(简单/预检请求、凭证请求 * 禁则、不透明错误)【一手】https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  2. expressjs/cors(GitHub / Express 中间件文档)【一手】https://github.com/expressjs/cors
  3. Spring CORS(WebMvcConfigurer / @CrossOrigin)· Spring 官方【一手】https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html
  4. FastAPI CORS 教程 / CORSMiddleware【一手】https://fastapi.tiangolo.com/tutorial/cors/
  5. flask-cors 配置文档【一手】https://flask-cors.readthedocs.io/en/latest/configuration.html
  6. django-cors-headers(PyPI)【一手】https://pypi.org/project/django-cors-headers/
  7. Vite server.proxy / Webpack devServer.proxy【一手】https://vite.dev/config/server-options
  8. nginx 配置 CORS(add_header、OPTIONS 处理、重复头坑)· LinuxCapable / HttpFixer【一手/权威】https://linuxcapable.com/how-to-configure-cors-in-nginx/
  9. AWS API Gateway CORS(REST / HTTP API)【一手】https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-cors.html
  10. Vercel 启用 CORS / Cloudflare 响应头修改【一手】https://vercel.com/kb/guide/how-to-enable-cors ;https://developers.cloudflare.com/rules/transform/response-header-modification/
  11. Supabase Edge Functions CORS【一手】https://supabase.com/docs/guides/functions/cors

安全与社区

  1. MDN CORS 安全章节(凭证请求 * 禁则、Origin 反射风险、null origin)【一手】https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  2. OWASP WSTG v4.2 · Testing CORS【一手】https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing
  3. OWASP A05:2021 / CWE-942【一手】https://owasp.org/Top10/A05_2021-Security_Misconfiguration/ ;https://cwe.mitre.org/data/definitions/942.html
  4. PortSwigger · Exploiting CORS misconfigurations for Bitcoins and bounties(James Kettle,交易所 XSSI 案例)【一手】https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties
  5. CVE-2025-34291(Langflow,CISA KEV、MuddyWater、RCE 链)· LaunchWeld 分析【厂商/博客,细节以 NVD/KEV 复核为准】https://launchweld.com/blog/2026-05-27/cve-2025-34291-langflow-cors-rce/
  6. CVE-2025-50579(Nginx Proxy Manager JWT 窃取)· NVD / GitHub issue #4509【一手】https://nvd.nist.gov/vuln/detail/CVE-2025-50579
  7. GHSA-hqm9-5xxw-4qxp(Grav CMS)· GitHub Advisory【一手】https://github.com/getgrav/grav/security/advisories/GHSA-hqm9-5xxw-4qxp
  8. Stack Overflow “No 'Access-Control-Allow-Origin' header”(canonical 答案)【社区】https://stackoverflow.com/questions/35553500/
  9. Hacker News CORS 讨论(“sea of confusion” 原话)【社区】https://news.ycombinator.com/item?id=48615744
  10. reduxjs/redux #3760(Firefox 跟踪保护伪装成 CORS 报错)【社区】https://github.com/reduxjs/redux/issues/3760
  11. Chrome Private Network Access preflight(WICG 提案)【一手】https://developer.chrome.com/blog/private-network-access-preflight

方法论说明

本报告采用横纵分析法:纵向沿时间轴还原 CORS 从同源策略(1995)到规范退役(2020)并并入 Fetch 的完整演进;横向以当前时间点为切面,对比 CORS 与其它跨域方案、框架实践与企业级处理模式;最后在交汇段把两条线索叠合,给出历史根源与未来剧本。该方法由数字生命卡兹克(Khazix)提出,融合语言学历时-共时分析、纵向-横截面研究设计与竞争战略分析。

评论互动

© 2026 王若风的技术博客 · Powered by Astro