CORS的2026年真相:你开的不是“跨域许可”,是一扇浏览器替你守门的信任裂缝
原创
www.klsec.com
www.klsec.com
昆仑AI安全实验室
2026年11月8日 01:20
广东
去阅读
2026年9月22日,open-vsx.org披露了一个CVE,CVSS评分9.3。
漏洞的根因写得很清楚:部署在CDN/边缘层的配置,把请求的Origin头原样反射进Access-Control-Allow-Origin,同时设置了Access-Control-Allow-Credentials: true。任何域的页面,都可以在已登录用户的浏览器里,向open-vsx.org发起带凭证的跨域请求,并读取响应。
被暴露的端点包括/user(登录名、头像、令牌URL)、/user/tokens、/user/namespaces,以及——/user/csrf。攻击者页面先读取CSRF token,然后调用/user/token/create,窃取一个携带发布和删除权限的个人访问令牌。攻击者可以部署恶意扩展,或删除合法扩展。
最讽刺的细节在后面:Open VSX的软件本身是正确的。它在唯一的地方设置了allowCredentials(true),绑定到一个从配置派生的精确origin,没有动态映射。是CDN/边缘层的配置绕过了应用层的正确实现。一个部署在“应用代码之外”的配置,把应用开发者写对了的安全边界,整个拆掉了。
这就是2026年CORS漏洞的精确切片:危险很少来自“CORS功能本身”,来自“配置CORS的那个人,和写应用代码的那个人,对‘谁可以被信任’的理解不一致”。
一、CORS不是“允许跨域”,是“浏览器替服务端执行一个信任声明”
同源策略(SOP)的核心规则很简单:一个源的脚本,不能读取另一个源的响应。CORS是SOP的协商机制——服务端通过Access-Control-Allow-Origin(ACAO)告诉浏览器:“这个请求的响应,可以交给发起请求的那个源读取。”
问题在于,这个“告诉”是由服务端生成的响应头承载的。浏览器不认识服务端的“意图”,它只认识这个响应头里的字符串。如果服务端把攻击者的Origin原样反射进ACAO,浏览器看到的就是“服务端授权了这个来源”。浏览器忠实地执行了它被告知的事情。它没有做错任何事。 错的是,告诉它的那个配置,不是应用开发者写的那一行。
Access-Control-Allow-Credentials: true是这条规则里最危险的那个开关。 它让浏览器在跨域请求中携带cookie、认证头等凭证。规范明确禁止ACAO: *和ACAC: true同时出现——浏览器会拒绝这种组合。但反射Origin完美地绕过了这个禁止:响应头里不是*,是一个具体的、看起来“合法”的origin,浏览器没有理由拒绝。
二、六条“缝”:2026年CORS配置错误的全谱
把2026年的CVE和实战案例收拢,CORS的“缝”集中在六个地方。每一条缝的根因,都是“服务端认为自己在授权一个来源,实际上授权了另一个东西”。
缝一:Origin反射——“你说是谁,就是谁”
最直接、最普遍的一条。服务端不做任何校验,把请求头里的Origin值原样写回Access-Control-Allow-Origin。Rybbit的CVE-2026-82287是这个模式的精确标本:2.7.0之前的版本反射任何请求origin,同时启用凭证,攻击者可以从任意网站发起带凭证的跨域请求,读取分析数据、账户信息,并执行已认证的状态变更操作。VulDB的分析把它归类为CWE-942,并指出这种配置“本质上使同源策略的保护失效”。
反射的本质是:服务端把“谁可以访问我”这个安全决策,交给了请求方自己来声明。
缝二:null Origin——一个“没有来源”的来源
Origin: null是一个合法的请求头值,它来自几种浏览器上下文:沙箱iframe(sandbox属性不带allow-same-origin)、本地HTML文件(file://)、以及一些跨源重定向场景。
null的特殊之处在于:攻击者可以轻易制造它。一个<iframe sandbox=\"allow-scripts\">里面的页面,发出的所有请求,Origin都是null。如果服务端信任null,攻击者不需要拥有任何域名,不需要绕过任何白名单,只需要在一个沙箱iframe里执行自己的脚本,就能获得“被信任的来源”的身份。
Netty的CVE-2026-6CQP-G7GG-8HR5是2026年最精确的null绕过案例。CorsHandler的shortCircuit()本应在请求到达应用之前拒绝未授权的跨域请求,但逻辑运算符的错误让这个保护可以被Origin: null完全绕过。一个以“安全控制”为设计目标的组件,被一个它本该拒绝的输入击穿了。 修复版本在配置中明确区分了“允许的origin列表”和“是否允许null”,而不是把两者混在一起处理。
缝三:子域名信任——“\*.target.com”是一张空头支票
允许*.target.com是一个常见的企业级CORS配置。逻辑是“我们的子域名是可信的”。但这个逻辑的前提是:每一个子域名,都真的由可信的团队控制。
2026年的现实是,子域名接管、GitHub Pages悬空CNAME、被遗忘的旧子域名指向第三方SaaS——这些攻击面让“\.target.com”变成了“任何能控制一个target.com子域名的人,都能发起带凭证的跨域请求”。HackerOne #1379054的案例链是:子域名接管 + CORS子域名信任 = 账户接管,不需要碰主应用*。
子域名信任的安全模型,依赖于一个CORS配置本身无法保证的东西:每一个子域名的生命周期管理是完整的。
缝四:前缀/后缀/预域名注入——“看起来像我们的域名”
如果校验逻辑是字符串匹配(startsWith、endsWith、includes),而不是精确的origin比较,绕过就变得极其廉价。
attacker-target.com通过endsWith(\"target.com\")。target.com.attacker.net通过startsWith(\"target.com\")。https://attacker.com.target.com通过includes(\"target.com\")——这是一个预域名注入,攻击者的域名是attacker.com,target.com只是它的子域名。
2026年的测试模块把这些变体标准化了:前缀绕过、后缀绕过、预域名注入,是CORS检测的固定项。它们的共同逻辑是:校验器在“字符串”层面做匹配,而浏览器在“origin”层面理解来源。一个字符串里包含target.com,不等于这个origin是target.com。
缝五:预检缓存投毒——浏览器替你“记住”了一个错误的授权
Chrome的CVE-2026-95374把CORS的攻击面从服务端配置拉到了浏览器实现层面。
Chrome的PreflightController在写入和查找预检缓存时,直接使用了request_initiator,没有把tainted origin标志纳入缓存键。攻击者诱导用户打开一个恶意页面,先发出一个经过跨源重定向的CORS请求,使Origin变为null的预检结果被缓存到发起者源的名下。随后,同一个发起者对同一个URL的未污染请求,会错误地跳过预检,直接发出非简单请求。
修复方式是把污染预检和未污染预检隔离到不同的缓存键里。这个漏洞的教学意义在于:CORS的安全假设是“预检缓存忠实地记录了服务端的授权”。当一个被污染的预检结果可以“代表”一个未被污染的origin时,浏览器缓存本身就成了攻击面。
缝六:Vary头覆盖——缓存代理把“你的响应”交给了“别人”
Netty的CVE-2026-59903走的是另一条缓存相关的缝,但根因完全不同。
CorsHandler#setVaryHeader静默地覆盖了后端应用已经设置的Vary头:
java
privatestaticvoidsetVaryHeader(finalHttpResponse response){ response.headers().set(HttpHeaderNames.VARY,HttpHeaderNames.ORIGIN);}
set()替换了所有现有的值。如果后端应用设置了Vary: Authorization或Vary: Cookie,以确保CDN按用户缓存响应,CorsHandler会把它覆盖成Vary: origin。缓存代理现在只根据URL和Origin来缓存,完全忽略了授权上下文。攻击者可以读取另一个用户的缓存敏感数据。
这条缝的精妙之处在于:它不是“CORS配置允许了错误的来源”,是“CORS组件在为了自己的功能(按Origin缓存)修改响应头时,破坏了另一个安全机制(按用户缓存)的假设” 。两个机制单独看都是对的,叠加在一起,缓存代理的信任模型被改变了。
三、从CORS到RCE:当“跨域读取”升级为“跨域执行”
大部分CORS漏洞的终点是“读取数据”。但2026年的SiYuan案例说明,当CORS配置错误被组合到一个有“本地服务暴露”或“客户端代码执行”能力的环境里时,它的危害可以从数据泄露跳到远程代码执行。
SiYuan的CVE-2026-34449(CVSS 9.8级别)的根因是Permissive CORS Policy:Access-Control-Allow-Origin: *加上Access-Control-Allow-Private-Network: true。攻击者页面利用这个配置,向本地运行的SiYuan API注入一段JavaScript snippet。这段snippet在Electron的Node.js上下文中执行,拥有完整的OS访问权限。不需要用户点击任何东西,只需要SiYuan在运行时用户访问了恶意网站。攻击者可以在Electron进程里执行任意系统命令。
这个链的关键在于CORS + Private Network Access的组合。Access-Control-Allow-Private-Network: true告诉浏览器:“公网页面可以访问本地网络服务。”当一个本地API接受来自任意origin的注入请求,而注入的内容又在Node.js上下文中执行时,CORS从一个“数据边界”问题,变成了一个“代码执行边界”问题。你的浏览器,被当成了攻击者向本地服务投递Payload的运输工具。
四、测试CORS时,你在测什么:不是“有没有ACAO头”,是“这个头授权了谁,在什么条件下”
2026年的CORS测试,需要从“看响应头”升级为“判断信任边界是否被一个攻击者可控的输入所定义” 。
第一,区分“有CORS”和“有可利用的CORS”。 一个端点返回ACAO: *,但没有ACAC: true,且不返回任何用户特定的数据,它不是漏洞。漏洞的判据是:ACAO的值是否由一个攻击者可控的输入决定(反射、null、前缀匹配),并且ACAC: true是否让这个跨域读取携带了受害者的凭证。两者同时成立,才是一个可提交的CORS漏洞。
第二,把Origin的构造当成攻击面的核心。 不要只发一个https://evil.com。测试null(用一个沙箱iframe加载你的页面)、https://evil.com.target.com(预域名注入)、https://target.com.evil.com(后缀)、https://attacker-target.com(前缀)、http://target.com(协议降级)。每一个变体对应一种校验逻辑的失效模式。
第三,把CORS当作链式利用的接驳器来评估。 一个单独的CORS误配可能是Medium。但把它和CSRF(读取/user/csrf,然后发起写操作)、IDOR(CORS让你能读任意用户的IDOR响应)、子域名接管(接管一个被信任的子域名)、或本地服务暴露(SiYuan的Electron上下文)组合起来,它就可能变成High或Critical。单独提交“存在CORS误配”通常拿不到高价,把链走通才是定价依据。
五、防御:CORS的安全实现,是一个“精确”问题,不是一个“功能”问题
2026年的这些CVE指向同一个防御原则:Access-Control-Allow-Origin的值,必须来自一个静态的、精确的、服务端控制的允许列表,绝对不能来自请求中任何用户可控的部分。
第一,禁止反射,禁止null,禁止前缀/后缀匹配。 如果Origin在允许列表里,返回那个精确的origin字符串。如果不在,不返回任何ACAO头。不要返回ACAO: null,除非业务有经过严格评估的、无法用其他方式满足的需求。不要用startsWith或endsWith——用精确的字符串相等比较。
第二,ACAC: true是一个需要被审慎对待的开关。 只有在端点确实需要跨域读取用户特定的、携带凭证的数据时才启用。公开的、不依赖会话的数据,不需要ACAC,也就不需要承担凭证泄露的风险。永远不要同时使用ACAO: *和ACAC: true——即使某些服务器实现允许这种组合,浏览器也会拒绝它,而反射Origin“绕过”这个拒绝的方式,恰恰是打开了一条更危险的路。
第三,审计“应用代码之外”的CORS配置。open-vsx.org的教训是:应用开发者写对了,CDN/边缘层写错了,漏洞照样成立。CORS头可能在多个地方被设置:应用框架、反向代理、CDN、API网关。最终的ACAO值,是所有这些层叠加之后的结果。安全审计必须覆盖最终生效的响应头,而不仅仅是应用代码里的cors()中间件。
第四,对缓存层保持警惕。 如果CORS响应会经过CDN或缓存代理,确认Vary头没有被CORS组件覆盖。一个Vary: origin的响应,如果同时携带了用户特定的数据,而缓存代理又忽略了Authorization或Cookie,缓存投毒就是结果。CORS和缓存是两个独立的机制,它们的交互需要被单独审计。
第五,本地服务/桌面应用的CORS配置,按“公网可访问”的威胁模型来对待。 SiYuan的案例说明,Access-Control-Allow-Private-Network: true把一个本地API暴露给了任意公网页面。桌面应用和本地服务的CORS配置,不应该假设“只有我自己会访问它” 。如果本地API不需要跨域访问,不设置CORS头,让浏览器的默认同源策略挡住一切。
写在最后
2026年的CORS漏洞,很少是“开发者不知道CORS有风险”那种形态了。反射Origin、信任null、前缀匹配、子域名通配符——这些模式在安全圈已经被反复讨论过。但CVE列表仍然在增长:Rybbit、open-vsx.org、Netty、Chrome预检缓存、SiYuan。
原因不在于“知识不够”。原因在于CORS的安全决策分散在太多层:应用代码、框架中间件、CDN配置、边缘规则、浏览器缓存。每一层都在假设“上一层已经处理好了”。open-vsx.org的应用代码是正确的,CDN层把它覆盖了。Netty的CORS组件是正确的,它为了自己的功能覆盖了Vary头,破坏了缓存层的假设。Chrome的预检缓存在大多数情况下是正确的,一个被污染的origin混进了未被污染的缓存键。
CORS的“缝”,从来不在某一行代码里。它在“谁有权定义信任”这个问题的答案,在层与层之间传递时被改变了的地方。 下次测CORS,不要只问“有没有ACAO头”。问:这个头的值,是从哪里来的?它经过了几层?最终执行这个授权的浏览器,和写下这个配置的那个人,对“谁被信任”的理解,是不是同一个东西?
严正声明
本文所述所有CORS漏洞分析、CVE案例及测试技术,仅用于已获得明确书面授权的安全测试、代码审计、红队演练和安全研究场景。CVE-2026-90882(open-vsx.org)、CVE-2026-82287(Rybbit)、CVE-2026-34449(SiYuan)、CVE-2026-95374(Chrome)、CVE-2026-59903(Netty)等漏洞的验证必须在授权范围内进行,并及时将受影响组件更新至修复版本。CORS误配的测试中提交的请求不应造成实际的数据外带或状态变更,应以证明跨域读取能力为限。未授权扫描、测试、攻击行为违反《中华人民共和国网络安全法》第27条及《刑法》第285条,将承担法律责任。技术无罪,边界是法律。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论