浏览器中的 CORS 边界:响应头、预检、凭证与缓存
用可复现的方法诊断跨源 Fetch,并区分 CORS、网络可达性、服务器授权、CSP 与 Permissions Policy。
BotBrowser Team
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)