返回知识中心
平台

浏览器中的 CORS 边界:响应头、预检、凭证与缓存

用可复现的方法诊断跨源 Fetch,并区分 CORS、网络可达性、服务器授权、CSP 与 Permissions Policy。

BotBrowser Team

文档中心

想直接进入 平台 文档吗?

这篇文章属于博客内容库。若你要步骤化配置、参考说明和持续更新,请直接进入对应 docs 分区。

浏览器分别处理响应可读性、CORS 共享、凭证和应用结果

Cross-Origin Resource Sharing(CORS)是浏览器控制的响应共享协议。它只回答一个问题:页面 JavaScript 能否读取另一个源返回的响应?CORS 不会让网络路由变得可达,不会授予服务器授权,也不会让第三方 API 自动可信。于是,网络面板中的 200 与页面脚本的 CORS 错误可以同时成立:交换成功,但 Fetch 把响应隐藏给脚本。 应分别记录页面源、目标 URL、HTTP 状态、响应头、控制台信息和页面可见状态。规范细节见 [Fetch CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol),实践说明见 [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)。 ## 源与简单请求 源由 scheme、host 和 port 组成。因此 本地 fixture 运行时会为页面和 API 分配不同的 localhost 端口,因此它们是跨源的。服务器通常返回 `Access-Control-Allow-Origin: `,浏览器会把它与发起页面的源比较;页面脚本不能任意伪造 `Origin`。 符合 safelist 的 `GET`、`HEAD` 或 `POST` 有时不需要预检,但响应仍需满足 CORS。`mode: 'no-cors'` 只会得到 opaque response,不能绕过限制,响应体和多数响应头不可读。`mode: 'same-origin'` 则拒绝跨源目标。 ## 预检交换 带 `Authorization`、JSON、非简单方法或自定义请求头的请求通常先发送 `OPTIONS`: ```http Origin: Access-Control-Request-Method: POST Access-Control-Request-Headers: authorization, content-type ``` 服务器需要返回匹配的源、方法和请求头: ```http Access-Control-Allow-Origin: Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: authorization, content-type Access-Control-Max-Age: 300 Vary: Origin ``` 浏览器在实际请求前验证预检。`OPTIONS` 被重定向、被认证中间件拒绝或返回 4xx,都可能阻止实际请求。`Access-Control-Allow-Methods` 不是用户授权;实际 endpoint 仍须执行权限检查。预检缓存还可能保留旧策略,诊断时应使用新的浏览器上下文。 ## 凭证与缓存 跨源 cookie 通常需要 `credentials: 'include'`、明确的 Allow-Origin 以及 `Access-Control-Allow-Credentials: true`。带凭证的可读响应不能使用 `*`。`SameSite`、`Secure`、cookie 范围、服务器授权和 CSRF 防护都是独立边界。 如果服务器按源动态选择 Allow-Origin,请返回 `Vary: Origin`,避免共享 CDN 把一个源的响应复用给另一个源。浏览器缓存、预检缓存、service worker、代理和 CDN 都可能改变观察结果。稳定 fixture 应使用新的 BrowserContext 并采集真实响应头,而不是为了通过测试修改生产缓存。 ## CORS、CSP 与 Permissions Policy 的区别 | 控制 | 核心问题 | 证据 | 不代表 | | --- | --- | --- | --- | | 网络/DNS | URL 是否可达? | DNS、TLS、连接 | 响应可读或已授权 | | CORS | JavaScript 能否读取响应? | `Access-Control-Allow-*`、Fetch 结果 | 服务器授权或业务完成 | | CSP | 文档允许连接哪些目标? | `connect-src`、报告、阻断 | CORS 许可 | | Permissions Policy | 文档或 iframe 能否使用某项能力? | 响应策略、`allow` | 用户授权、设备或 CORS | [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) 可能先通过 `connect-src` 阻止连接,因而不存在可分析的 CORS 响应。[Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy) 管理文档和 iframe 的浏览器能力,不会开放 API 响应体。不要为了隐藏一种错误而弱化另一种策略。 ## 自有 control/candidate fixture 准备自有的 `https://api.example.test/cors-fixture` ` https://app.example.test/control` 和 `https://app.example.test/candidate` 。两页使用同一条可达路由与合成响应体,只有 control 响应包含 `Access-Control-Allow-Origin: ` ,candidate 故意不包含。通过按钮明确触发请求,并显示状态。 ```js import { chromium } from 'playwright'; import http from 'node:http'; import assert from 'node:assert/strict';

let pageOrigin = ''; let apiOrigin = ''; const observations = []; const listen = server => new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const close = server => new Promise(resolve => server.close(resolve)); const api = http.createServer((request, response) => { const name = request.url.slice(1); if (name === 'control') response.setHeader('Access-Control-Allow-Origin', pageOrigin); observations.push({ name, status: 200, allowOrigin: response.getHeader('Access-Control-Allow-Origin') || null }); response.writeHead(200, { 'Content-Type': 'application/json' }); response.end('{"fixture":true}'); }); const pageServer = http.createServer((request, response) => { const name = request.url.slice(1); response.end(<button>Run CORS check</button><output data-testid="cors-status"></output><script> document.querySelector('button').onclick = async () => { try { await fetch('${apiOrigin}/${name}'); document.querySelector('output').textContent = 'CORS_ALLOWED'; } catch { document.querySelector('output').textContent = 'FETCH_UNCLASSIFIED'; } };</script>); }); await listen(api); await listen(pageServer); const apiPort = api.address().port; const pagePort = pageServer.address().port; pageOrigin = http://127.0.0.1:${pagePort}; apiOrigin = http://127.0.0.1:${apiPort}; const browser = await chromium.launch(); const context = await browser.newContext(); context.setDefaultNavigationTimeout(8_000); async function runCase(name) { const page = await context.newPage(); try { await page.goto(${pageOrigin}/${name}); await page.getByRole('button', { name: 'Run CORS check' }).click(); await page.locator('[data-testid="cors-status"]').waitFor({ state: 'visible', timeout: 5_000 }); return await page.locator('[data-testid="cors-status"]').textContent(); } catch { return 'UNKNOWN'; } finally { await page.close(); } } try { const control = await runCase('control'); const candidate = await runCase('candidate'); assert.equal(control, 'CORS_ALLOWED'); assert.equal(candidate, 'FETCH_UNCLASSIFIED'); assert.deepEqual(observations.map(({ name, allowOrigin }) => ({ name, allowOrigin })), [ { name: 'control', allowOrigin: pageOrigin }, { name: 'candidate', allowOrigin: null } ]); } finally { await context.close(); await browser.close(); await close(pageServer); await close(api); }

control 应在读取合成响应体后显示 `CORS_ALLOWED`。candidate 在 Fetch 拒绝后显示 `FETCH_UNCLASSIFIED`;该状态本身不能归因 CORS。只有在自有 API 日志证明两个请求均到达、均返回相同响应体和 200、candidate 响应故意缺少 Allow-Origin,且 control 以页面精确 origin 成功读取时,才确认 `CORS_BLOCKED`。

导航、状态或服务器失败时结果保持 `UNKNOWN`,不能归因网络或 CORS。导航上限为八秒,每个 page 在 cleanup 中关闭。

## 决策表

| 观察                | 首要责任方  | 下一步                           | 安全结论                    |
| ------------------- | ----------- | -------------------------------- | --------------------------- |
| DNS、TLS 或连接失败 | 网络/平台   | 路由、代理、服务健康             | 没有得到 CORS 响应          |
| 预检 4xx/5xx        | API/平台    | OPTIONS 路由、认证、方法和请求头 | 预检交换失败                |
| 200 但脚本无法读取  | API/浏览器  | Origin、凭证、暴露响应头         | 共享被拒绝或响应 opaque     |
| cookie 缺失         | 认证/浏览器 | credentials、SameSite、Secure    | CORS 不会让 cookie 自动有效 |
| 不同源结果不同      | API/缓存    | allow-list 和 `Vary: Origin`     | 策略或缓存路径不同          |
| CSP violation       | Web 安全    | `connect-src` 与报告             | 这是 CSP 阻断,不是 CORS    |

BotBrowser 能验证什么
BotBrowser 的受控 BrowserContext 可以运行获授权的自有 control/candidate CORS fixture,隔离合成 cookie 与 storage,并在声明的浏览器版本中比较可见的 Fetch 结果。[多账号隔离文档](https://botbrowser.io/docs/identity/multi-account-isolation/) 说明了该上下文边界。

对于 CORS 验证,BotBrowser 可以执行 control/candidate 对比,但不能配置 API 响应或绕过浏览器的共享规则。
BotBrowser 不会编写或部署 CORS 响应头,不授予服务器权限,不修复第三方 API,不改变源,也不会绕过浏览器 CORS 强制执行;它同样不能证明远程业务已提交。服务器、CDN、凭证、CSP、Permissions Policy 和应用状态仍由各自系统负责。
操作清单

1. 声明页面源和 API 源、方法、请求头、credentials 与期望结果。
2. 先检查 DNS、TLS、代理和路由,再分析策略错误。
3. 采集 Origin、预检、状态、CORS 头、`Vary` 与缓存信息。
4. 分开记录 CORS、CSP、Permissions Policy、用户授权和服务器授权。
5. 使用自有 fixture,导航 8 秒、状态 5 秒,并在结束时清理上下文。
6. 不要合并网络错误、导航错误、超时、预检失败和 CORS 阻断。
   **作者:** BotBrowser 团队
   参阅 [跨源隔离指南](/zh/blog/cross-origin-isolation-and-shared-memory-requirements/) 与 [Permissions Policy 指南](/zh/blog/permissions-policy-for-embedded-browser-features/)。

## Sources

- Fixture URL (example only): https://app.example.test`
- Fixture URL (example only): https://app.example.test/control`

- [Fetch CORS protocol](https://fetch.spec.whatwg.org/#http-cors-protocol)
- [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
- [MDN CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
- [MDN Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Permissions_Policy)
#CORS#跨源请求#Fetch#Web 安全

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。