COOP与COEP跨域隔离:开之前先算清代价

一、跨域隔离要解决的到底是什么问题

浏览器里的同源策略是一道很粗的墙:不同源之间的脚本不能读彼此的 DOM、不能读对方的响应体。但有些攻击不需要读响应体,只需要「让请求发出去」或者「知道请求成功了没有」。最典型的是 Spectre 类侧信道攻击——攻击者通过测量内存访问时间,推断出另一个源的内存内容。这类攻击绕过了同源策略的全部假设,因为同源策略管的是「访问」,不是「执行」。于是浏览器厂商引入了另一层防护:跨域隔离(Cross-Origin Isolation)。

跨域隔离不是让页面更安全这种模糊的说法,它有非常明确的三个前提条件,全部满足才能启用一批高精度计时和高性能 API:页面必须处于「跨源隔离」状态,也就是 window.crossOriginIsolated === true。要达成这个状态,需要同时满足两个响应头:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

COOP 负责隔离浏览上下文组,让跨源窗口之间失去 window.opener 引用、无法通过 postMessage 随意通信。COEP 负责隔离子资源,要求页面加载的每一个跨源资源都必须显式声明允许被嵌入。二者合起来,保证页面的整个执行环境(包括所有子资源)都在同一个源的控制之下。

满足条件之后能拿到什么?最关键的是 SharedArrayBuffer,因为跨线程共享内存是 Spectre 攻击的核心工具。其次是更精确的 performance.now() 精度(从被模糊化的 100 微秒级恢复到 5 微秒级),以及一些高性能图形与计算 API。如果只是想做一个普通的资讯站,你很可能一辈子用不到这些。

二、为什么大部分站长应该慎重

这里有个很常见的误判:看到安全建议里说「开启 COOP 和 COEP 能提高安全性」,于是直接加进 Nginx 配置,结果页面白屏或者样式全乱。原因是 COEP 的 require-corp 模式非常严格,它不是「建议」而是「强制」:任何没有明确授权的跨源资源都会被浏览器拒绝加载。

会被拦下的典型资源包括:第三方 CDN 上的 jQuery、Google Fonts 的 CSS 与字体、外链的广告脚本、内嵌的第三方视频播放器、微信/微博的分享组件、统计脚本里动态插入的 iframe。这些服务商大多没有适配 COEP,它们的响应里不带 Cross-Origin-Resource-Policy 头,于是全部被拒。

更麻烦的是排查成本。COEP 拦截在控制台里只报一条 net::ERR_BLOCKED_BY_RESPONSE,页面不会崩溃,但会「看起来还行、功能少一半」——某个按钮点了没反应,某张图不显示。这种半坏状态比彻底白屏更难发现。

所以第一件事不是改配置,而是问自己:我的站需不需要 SharedArrayBuffer?如果答案是不需要,那就不要开 COEP。COOP 可以单独开(它只影响弹窗与 opener 关系),风险小得多,但也要注意它会让「第三方登录弹窗回传 token」这类流程失效。

三、只想要 COOP:低风险的一步

如果你的站没有依赖弹窗回传的登录流程,单独启用 COOP 是相对安全的:

add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Opener-Policy "same-origin-allow-popups" always;

这两个值不能同时生效,只能选一个。same-origin 最严格,任何跨源弹出窗口都会失去与打开者的引用关系。same-origin-allow-popups 则允许页面自己打开的弹窗保持 opener 关系,适合「点按钮打开第三方授权页、用 postMessage 回传」的场景,代价是这种弹窗仍能通过 opener 反向操作你的页面,防护强度略低。

选择依据很实际:你的站有没有 OAuth 登录弹窗、有没有第三方客服挂件会弹出新窗口。有就选后者,没有就选前者。改完要专门测一遍:打开一个外链(比如友情链接),确认 target="_blank" 的页面能正常打开,且新页面里的 window.opener 是 null。如果新页面的脚本因为拿不到 opener 而报错,说明它依赖这个引用,需要评估影响。

又一个细节:add_header 在 Nginx 里有个著名的行为——只要内层 location 里出现了任何 add_header,外层的所有 add_header 都会被丢弃。很多站点在静态资源 location 里加了 Cache-Control,结果主配置里的安全头在那些路径上全部失效。解决办法是用 always 参数(它只影响错误响应,不解决继承问题),或者把安全头集中到一个 include 文件,在每个带 add_header 的 location 里都 include 一遍。这一点值得单独检查,因为它会静默地让「我以为加了」变成「其实没加」。

四、真的要开 COEP:先做资源盘点

如果确实需要 SharedArrayBuffer(比如做在线视频剪辑、WebAssembly 重型计算、端侧模型推理),那就得开 COEP,而且必须先把资源盘点清楚。

盘点的方法是打开页面,在控制台里把所有被 COEP 拦下的请求列出来。可以直接用脚本统计:

performance.getEntriesByType('resource')
  .map(function(r) { return r.name; })
  .filter(function(n) { return n.indexOf(location.origin) !== 0; })
  .filter(function(n, i, a) { return a.indexOf(n) === i; });

这段代码给出页面上所有跨源资源。注意它只能拿到「成功加载」的,被拦下的不会出现在 getEntriesByType 里,所以更可靠的做法是先在测试环境开启 COEP,然后收集控制台错误,或者用 report-to 上报。

盘点结果通常分三类,处理方式完全不同:

资源类型典型例子处理
自己域名下的静态资源CSS、JS、图片、字体无需处理,同源天然通过
支持 CORP 的第三方部分大厂 CDN、部分字体服务加 crossorigin 属性即可
不支持 CORP 的第三方老 CDN、广告脚本、统计代码无法适配,只能下架或代理回源

第三类是真正的麻烦。可行的路径只有三条:一是把这些资源复制到自己域名下托管(字体尤其适合,还能顺带解决国内字体 CDN 不稳定);二是用服务端反向代理,让它们变成同源请求(要注意版权与授权条款);三是干脆不用这个资源。

五、CORP 与 CORS 的分工

开启 COEP 之后,跨源资源要么带 Cross-Origin-Resource-Policy 响应头,要么走 CORS 流程。这两个机制经常被混淆,需要说清楚。

Cross-Origin-Resource-Policy 由被请求方在响应头里声明,取值有三个:same-origin(只允许同源引用)、same-site(允许同站,比如 a.example.com 引用 b.example.com)、cross-origin(允许任何站点引用)。它是「资源方主动声明可以被谁用」,不需要请求方做任何事。

CORS 则是请求方发起的协商:请求方带 Origin 头,响应方回 Access-Control-Allow-Origin。在 COEP 环境下,如果资源带了正确的 CORS 头,也算通过——但前提是引用它的元素上要有 crossorigin 属性,否则浏览器按 no-cors 模式请求,拿不到 CORS 授权。

<img src="https://cdn.example.net/a.jpg" crossorigin="anonymous">
<script src="https://cdn.example.net/lib.js" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.example.net/a.css" crossorigin="anonymous">

注意 crossorigin="anonymous" 与 crossorigin="use-credentials" 的区别:前者不带 cookie,后者的响应必须回 Access-Control-Allow-Credentials: true 且 Allow-Origin 不能是通配符。字体资源总是走 CORS(这是 CSS 字体规范的要求),所以字体最容易在 COEP 下出问题。

六、给自己站点加 CORP 头

反过来,如果你的站提供资源给别人的页面用(比如你有一个公开的 JS 库、一套图片 CDN),可以主动加 CORP 头,让使用你资源的站点能开 COEP:

location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2?)$ {
    add_header Cross-Origin-Resource-Policy "cross-origin" always;
    add_header Access-Control-Allow-Origin "*" always;
    expires 30d;
    add_header Cache-Control "public, max-age=2592000, immutable";
}

这里要注意前面提到的 add_header 继承问题:这个 location 里出现了三个 add_header,主配置里的 COOP、COEP、X-Content-Type-Options 都不会继承过来。所以如果主站也开了 COEP,必须在这个 location 里把它们补一遍。这是配置类问题里最典型的「看起来配了、实际没生效」。

对于不想让外人引用的资源(比如用户上传的图片),反而应该加 same-origin,防止被别的站热链或者在 COEP 环境下被滥用。这里要区分「防止盗链」和「CORP」——前者是省钱,后者是安全,但副作用是同一个:别的站引用你的图会失败。

七、用报告模式先观察再上线

COEP 支持 report-only 模式,只上报不拦截,这是最稳妥的上线方式:

add_header Cross-Origin-Embedder-Policy-Report-Only "require-corp" always;
add_header Cross-Origin-Opener-Policy-Report-Only "same-origin" always;
add_header Reporting-Endpoints 'default="https://www.example.com/csp-report"' always;

上报接收端是一个你自己的接口,收到的 JSON 里有 blockedURL、type、disposition 等字段。部署后观察一周,统计 blockedURL 的域名分布,就知道还有多少资源没适配。

判断是否可以正式开启的标准要具体,不能是「看着差不多了」。建议是:连续七天 disposition: enforce 的上报数为零,且在这一周里覆盖了主要页面模板(首页、文章页、搜索结果页、用户中心)。如果某个模块一周没被访问过,报告模式自然也报不出它的问题,正式开启后就可能踩到。

收到报告后不要立刻加白名单——白名单在 COEP 里不存在。唯一的处理是让资源方加头,或者自己代理。如果有人建议「加个 iframe 绕一下」,那等于放弃了隔离的意义,不如不开。

八、常见故障与处置

开了 COEP 后图片全部裂开:八成是图片走的是外部图床。图床一般不给 CORP 头,唯一的办法是迁移到自己域名。如果你用了对象存储,可以在存储桶的跨域设置里加 CORP,云厂商的控制台一般都有这一项。

开了 COEP 后广告位变空白:AdSense 等广告脚本通常不支持 COEP。这不是配置问题,是产品能力问题。这类站点就不该开 COEP,除非你能接受放弃广告收入。

crossOriginIsolated 一直是 false:先确认两个头都真的下发了。用 curl -sI 看最终响应,注意要跟随重定向(-L),因为有些站首页会 301,而头只加在了某一层。另一个常见原因是页面里嵌了一个不带 COEP 的 iframe——iframe 的响应也必须带 COEP,否则整个页面失去隔离状态。这个问题最隐蔽,因为主文档的头完全正确。

开了 COOP 后第三方登录回不来:回到 same-origin-allow-popups,或者把登录流程改成整页跳转而不是弹窗。后者其实更安全,也更好做移动端适配。

九、小结

跨域隔离是一把针对性很强的工具,它保护的是「页面执行环境不被侧信道窥探」,而不是「网站不被攻击」。对绝大多数内容站、博客、企业站来说,它带来的收益接近于零,而代价是一整套第三方资源的适配工作。

真正值得做的是三件成本极低的事:单独加 COOP(无弹窗依赖时)、给自建静态资源加 CORP 头(让别人能安全引用你)、以及用 report-only 模式观察一段时间再决定。判断标准也很清晰:如果你在代码里搜不到 SharedArrayBuffer,也没有需要高精度计时的功能,那就不开 COEP。安全配置的第一原则是「为了具体威胁而存在」,没有威胁的配置只会变成未来的故障源。

Last modification:November 5th, 2026 at 07:27 pm

Leave a Comment