cross-origin-cors-reference/cors-config-generator
GitHub: cross-origin-cors-reference/cors-config-generator
根据实际 API 需求为十五种服务端技术栈生成可直接粘贴的 CORS 配置,并提供实时安全审查和浏览器行为预览的单页工具。
Stars: 0 | Forks: 0
# cors-config-generator
这是一个单页工具,它可以根据您的 API 的实际需求描述,为十五种服务端技术栈生成正确的 CORS 配置,审查每个选择的安全后果,并详细分析浏览器将如何处理您描述的请求。
无需构建步骤。无需包管理器。无需联网。只需克隆仓库,在浏览器中打开 `index.html` 即可运行——甚至直接通过 `file://` 协议加载也能正常工作。
## 目录
- [它是什么](#what-it-is)
- [为什么会有这个工具](#why-it-exists)
- [环境要求](#requirements)
- [运行方式](#running-it)
- [工作原理](#how-it-works)
- [策略计划](#the-plan)
- [拒绝机制](#the-refusals)
- [安全审查](#the-security-review)
- [浏览器预览](#the-browser-preview)
- [可分享的 URL](#shareable-urls)
- [实操示例:带凭证的精确白名单](#worked-example-exact-allowlist-with-credentials)
- [Nginx 输出](#nginx-output)
- [Express 输出](#express-output)
- [输入参数参考](#input-reference)
- [API 接口说明](#api-surface)
- [支持的技术栈及其注意事项](#supported-stacks-and-their-caveats)
- [测试](#tests)
- [局限性以及刻意不做的事情](#limitations-and-what-this-deliberately-does-not-do)
- [延伸阅读](#further-reading)
- [贡献](#contributing)
- [许可证](#licence)
## 它是什么
基于同一个策略引擎构建的三合一工具:
1. **配置生成器。** 选择一个技术栈,描述您的 origins、方法、headers 和凭证要求,即可获得可直接粘贴到 Nginx、Apache、Express、Fastify、FastAPI、Django、Flask、Spring Boot、ASP.NET Core、Go `net/http`、Rails、Cloudflare Worker、AWS API Gateway、Caddy 或 Envoy 中的配置。每段代码片段都附带了背后的推理逻辑,而不仅仅是简单的指令。
2. **安全审查。** 一个实时面板会对当前配置进行分级——严重、警告、提示——并为每一项发现提供具体的解释。它能捕获通配符结合凭证、白名单中包含 `null`、未锚定的后缀和正则表达式规则、暴露了包含凭证的 headers、范围过大的 `Access-Control-Allow-Headers`、超过浏览器限制的 `max-age` 值,以及大约三十种其他潜在问题。
3. **浏览器模拟器。** 描述一个请求——包括方法、`Content-Type`、自定义 headers、是否携带凭证——该工具就会展示是否会触发预检请求及其原因、预检是否会通过、真实请求是否会被发送,以及您的 JavaScript 是否能够读取响应。这是用于教学的部分;而生成的配置则是您可以直接复制粘贴的部分。
## 为什么会有这个工具
CORS 配置通常是复制粘贴的,而不是从头编写的。被复制的版本通常至少包含以下错误之一:
- 在 `Access-Control-Allow-Credentials: true` 旁边使用了 `Access-Control-Allow-Origin: *`,没有任何浏览器会接受这种组合——请求会直接失败,而且这种配置表面上看起来权限很宽松,导致没人会去怀疑它;
- 未经验证地直接反射请求的 `Origin`,这比通配符的安全性更差,因为它*确实*会与 cookies 一起生效;
- 缺少 `Vary: Origin`,导致 CDN 可能会将一个客户的 `Allow-Origin` header 错误地分发给另一个客户;
- 预检请求到达了身份验证层并被返回 401,因为预检请求在设计上本来就不携带凭证;
- 在 Nginx 的 `location` 块中添加了 `add_header`,导致静默删除了从 `server` 块继承的所有 CORS headers。
这些错误都不会产生有用的报错信息。CORS 失败对 JavaScript 来说是完全不透明的:`fetch()` 会直接拒绝并抛出一个光秃秃的 `TypeError`,其中不带任何状态码、headers 或响应体。唯一的诊断信息是控制台中的一条消息,而调用它的代码根本无法读取到这条消息。
这个工具的存在,是为了确保您复制粘贴的配置从一开始就是正确的,并且配置正确的原因也会直接显示在旁边。
## 环境要求
- **使用该应用:** 任何主流浏览器即可。Chromium、Firefox、Safari 及其衍生版本都没问题。无需服务器,无需网络访问,无需安装扩展。
- **运行测试:** 需要 Node.js 18 或更高版本,以支持 `node:test`。不需要其他任何东西——测试套件仅使用内置模块。
无需进行任何安装。它没有发布在任何包管理器仓库中,也没有 `package.json` 文件。
## 运行方式
```
git clone
cd cors-config-generator
```
然后打开 `index.html`:
```
xdg-open index.html # Linux
open index.html # macOS
start index.html # Windows
```
如果您更喜欢使用真实的 origin,可以使用您已有的任何工具来托管该目录:
```
python3 -m http.server 8000
```
该页面刻意编写为**经典脚本,而非 ES modules**。通过 `file://` 加载的文档具有一个不透明的 origin,因此每一次 ES module 的获取请求都会被视为跨域并被同源策略阻止——这正是本工具要解释的机制。`src/` 中的三个文件都包装在一个小型的 UMD 垫片中:在浏览器中它们挂载到 `window` 对象上,而在 Node 环境下它们表现为 CommonJS 模块,这也是测试套件加载它们的方式。
## 工作原理
### 策略计划
一切都源于同一个函数:`CorsCore.buildPlan(config)`。它接收原始的表单输入,并生成一个正确的实现必须发送的一组响应 headers。生成器永远不会自行做出策略决定;它们只是将计划渲染为各自的语法。这就是为什么在 UI 中切换技术栈只会改变语法和注意事项,而永远不会改变语义的原因。
该计划还计算了 UI 所依赖的两个派生事实:
- **`dynamic`** —— 生成的 `Access-Control-Allow-Origin` 是否依赖于请求的 `Origin` header。除了字面量通配符和单一固定 origin 外,在其他所有模式下均为 true。
- **`vary`** —— 是否发送 `Vary: Origin`。只要 `dynamic` 为 true,就会强制开启,因为如果忽略了 `Origin` 的共享缓存可能会存储为某个 origin 生成的响应,并将其重放给另一个 origin。
### 拒绝机制
有些配置是根本无法生效的。生成器不会直接输出它们然后任由浏览器在运行时拒绝,而是会覆盖输入并记录一个 `refusal`,解释它做了哪些修改:
| 输入 | 生成器的处理方式 |
| --- | --- |
| 通配符 origin + 凭证 | 丢弃 `Access-Control-Allow-Credentials`。浏览器会拒绝在任何携带凭证的请求中使用 `*`。 |
| `Allow-Headers: *` + 凭证 | 将 `*` 替换为明确的列表。在携带凭证时,`*` 会被当作字面字符串进行比较。 |
| `Allow-Methods: *` + 凭证 | 同上,原因相同。 |
| `Expose-Headers: *` + 凭证 | 移除字面量 `*`。 |
| `Expose-Headers` 中包含 `Set-Cookie` | 将其移除。浏览器会无条件地从暴露的集合中剔除 `Set-Cookie`。 |
| 在动态白名单下关闭 `Vary: Origin` | 强制添加。 |
每一个拒绝记录都会显示在 UI 中生成代码片段的上方,并在生成配置顶部的注释横幅中作为 `NOTE:` 行出现,因此这些推理逻辑在复制粘贴后依然得以保留。
### 安全审查
`CorsCore.analyze(config)` 返回一个排序后的发现结果数组,每个结果包含一个 `level`(`critical`、`warning` 或 `note`)、一个 `title`、一段专为必须去捍卫该决策的人编写的 `detail`,有时还会包含一个指向更长说明的 `link`。
正则表达式 origin 规则会接受额外的检查:该模式会被编译,并使用 `https://evil.com`、`https://example.com.evil.com` 和 `null` 进行试探。如果其中任何一个通过了,该发现的级别就会是严重。这能捕获最常见的 origin 验证缺陷——未锚定或未转义的模式。
安全级别永远不会仅仅通过颜色来指示。每个发现都带有一个文本标签,写着 `Critical`、`Warning` 或 `Note`,列表上方的摘要行会给出各项的计数。
### 浏览器预览
`CorsCore.predict(config, request)` 的实现非常贴近 Fetch 标准中的 CORS 协议,足以作为一种教学工具。它会按顺序回答以下问题:
1. **这是一个简单请求吗?** 只有当方法是 `GET`、`HEAD` 或 `POST`;每个 header 都在 CORS 安全名单中(`Accept`、`Accept-Language`、`Content-Language`、`Content-Type`、简单的字节范围 `Range` 以及客户端提示);并且,如果设置了 `Content-Type`,其值是 `application/x-www-form-urlencoded`、`multipart/form-data` 或 `text/plain` 之一时,它才是简单请求。否则就会发送预检请求,并且工具会列出所有的触发原因。
2. **预检是否通过?** Origin 必须与白名单匹配。状态码必须是 2xx——无论附加了什么 headers,3xx 或 4xx 都会导致预检失败。请求的方法必须在 `Allow-Methods` 中。每一个非安全名单内的 header 都必须在 `Allow-Headers` 中,唯一的例外是 **`Authorization` 永远不会被 `*` 通配符覆盖**,必须明确指出。如果请求携带了凭证,`Allow-Origin` 不能是 `*`,并且必须存在 `Allow-Credentials: true`。
3. **实际请求是否成功?** 被阻止的响应并不意味着请求没有发送:对于简单请求,当浏览器拒绝将响应交给您的代码时,服务器其实已经执行了该请求。预览会明确说明这一点,因为这正是 CORS 问题与安全控制之间的区别所在。
4. **JavaScript 可以读取哪些响应 headers?** 安全名单内的响应 headers(`Cache-Control`、`Content-Language`、`Content-Length`、`Content-Type`、`Expires`、`Last-Modified`、`Pragma`)总是可读的。其他的只有在被包含在 `Access-Control-Expose-Headers` 中时才可读——而一个未被暴露的 header 读取时会返回 `null`,但不会导致请求失败,这是一种截然不同且经常被混淆的失败模式。
### 可分享的 URL
完整的配置会使用短键名编码到 URL 的片段(fragment)中,因此一个配置可以直接通过工单链接或代码审查评论来分享。示例:
```
index.html#s=express&m=suffix&x=.example.com&p=https&v=GET,POST,DELETE&h=Content-Type,Authorization&c=1&a=600&t=1&y=1
```
状态会在页面加载和 `hashchange` 时恢复,并在您编辑时通过 `history.replaceState` 进行重写,因此不会让每一次按键都塞满后退按钮的历史记录。该片段永远不会离开浏览器——它不会在 HTTP 请求中被发送。
## 实操示例:带凭证的精确白名单
下方两段代码片段所使用的设置:
| 设置项 | 值 |
| --- | --- |
| 允许的 origins | `https://app.example.com`, `https://admin.example.com` |
| 方法 | `GET`, `POST`, `PUT`, `DELETE` |
| 请求 headers | `Content-Type`, `Authorization` |
| 暴露的 headers | `X-Request-Id` |
| 凭证 | 是 |
| `Max-Age` | 600 |
| 短路 `OPTIONS` | 是 |
由于包含两个 origins,这意味着白名单是动态的,因此会强制开启 `Vary: Origin`。
### Nginx 输出
`http { }` 上下文:
```
# http { } context. nginx omits an add_header whose value is the empty
# string, so an unmatched origin simply gets no CORS headers at all.
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
```
`server { }` 上下文:
```
# CORS policy
# allowed origins : https://app.example.com, https://admin.example.com
# methods : GET, POST, PUT, DELETE
# request headers : Content-Type, Authorization
# exposed headers : X-Request-Id
# credentials : yes
# max-age : 600s
server {
listen 443 ssl;
server_name api.example.com;
location /api/ {
# Preflight short-circuit. This never reaches the upstream.
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 600 always;
add_header Content-Type "text/plain; charset=utf-8" always;
add_header Content-Length 0 always;
return 204;
}
# Actual requests. Every CORS header the response needs must be
# repeated in this block: see the inheritance note below.
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Expose-Headers "X-Request-Id" always;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
请注意,这段配置正确处理了三个手写 Nginx CORS 配置通常会忽略的细节:白名单放在一个 `map` 中,因此未匹配的 origin 会产生一个空变量,Nginx 会直接省略该 header;每一个 `add_header` 都带有 `always`,因此即使是 401 或 500 错误也会附带 CORS headers,浏览器能报告真实的状态码而不是笼统的 CORS 错误;所有的 headers 都在 `location` 块内重复声明了一遍,因为只有当内层块没有声明任何属于自己的 `add_header` 时,Nginx 才会从外层块继承 `add_header`。
### Express 输出
```
// CORS policy
// allowed origins : https://app.example.com, https://admin.example.com
// methods : GET, POST, PUT, DELETE
// request headers : Content-Type, Authorization
// exposed headers : X-Request-Id
// credentials : yes
// max-age : 600s
// Exact-match allowlist. A Set gives constant-time lookup and makes it
// obvious that this is a membership test, not a substring test.
const ALLOWED_ORIGINS = new Set([
"https://app.example.com",
"https://admin.example.com",
]);
function allowedOrigin(origin) {
if (!origin) return null;
return ALLOWED_ORIGINS.has(origin) ? origin : null;
}
function cors(req, res, next) {
// Vary must be set before the allowlist check, on every response.
// If it is only set on allowed origins, a cache can still store the
// header-less rejection under a key that ignores Origin.
const value = allowedOrigin(req.headers.origin);
if (value === null) {
res.vary("Origin");
// Not on the allowlist: emit no CORS headers and let the request
// continue. Rejecting here would hide real 401s behind CORS errors.
return next();
}
res.vary("Origin");
res.setHeader("Access-Control-Allow-Origin", value);
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Expose-Headers", "X-Request-Id");
if (req.method === "OPTIONS") {
// Only treat it as a preflight when the browser says it is one.
if (req.headers["access-control-request-method"]) {
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Max-Age", "600");
res.setHeader("Content-Length", "0");
// 204 with no body. Any 3xx or 4xx fails the preflight outright,
// whatever headers are on it.
res.writeHead(204);
return res.end();
}
}
return next();
}
const express = require("express");
const app = express();
// Mount before the body parser and before any router, so a preflight is
// answered without running authentication or reading a body it will not have.
app.use(cors);
```
Express 生成器始终会输出一个**动态 origin 回调**,绝不会使用 `origin: true`。它还会提供第二段代码片段,展示它所替代的反模式,从而使两者的区别一目了然:
```
// This is the shape to avoid. It reflects any Origin and pairs it with
// credentials, which is strictly weaker than a wildcard: a wildcard at
// least cannot be used with cookies.
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", req.headers.origin); // unchecked
res.setHeader("Access-Control-Allow-Credentials", "true");
next();
});
```
### 针对此配置的示例安全审查
```
0 critical, 0 warning, 5 notes.
NOTE The origin is reflected only after a check
The generated code echoes the request Origin into Access-Control-Allow-Origin,
but only once the origin has passed the allowlist. Reflecting an unchecked
Origin is equivalent to a wildcard that also works with credentials.
NOTE Vary: Origin is emitted automatically
The response now differs per Origin. Without Vary: Origin, any shared cache can
serve the Allow-Origin header minted for one site to a different one.
NOTE Cookies and TLS client certs will be sent
With credentials enabled the allowlist is an authentication boundary, not a
convenience setting: an allowed origin can act as the logged-in user.
NOTE Preflights are answered with 204
A preflight needs a 2xx status and no body. 204 is the cheapest correct answer.
NOTE Nginx discards inherited headers per block
add_header directives are inherited only if the child block declares none of
its own.
```
如果在通配符或整个域名后缀的 origin 集合中加入 `DELETE`,就会出现警告;如果将 `null` 加入白名单,或者请求在携带凭证的同时使用通配符,就会出现严重警告。
### 示例浏览器预览
请求:`PUT`,`Content-Type: application/json`,自定义 header `Authorization`,携带凭证,来自 `https://app.example.com`。
```
Allowed: Preflight passes, then the request succeeds.
Preflight triggered because:
- PUT is not one of GET, HEAD or POST.
- Authorization is not a CORS-safelisted request header.
- Content-Type "application/json" is outside the safelisted set
(application/x-www-form-urlencoded, multipart/form-data, text/plain).
1 classify This is not a simple request, so the browser sends an OPTIONS preflight first.
2 preflight-request OPTIONS with Origin: https://app.example.com, Access-Control-Request-Method:
PUT, Access-Control-Request-Headers: authorization, content-type. The
preflight itself never carries cookies, even though the real request will.
3 preflight-status The server answers 204 No Content. Any 3xx or 4xx here fails the preflight
regardless of the headers on it.
4 preflight-origin Access-Control-Allow-Origin: https://app.example.com (echoed after the
allowlist check).
5 preflight-method PUT is covered by Access-Control-Allow-Methods.
6 preflight-header Authorization is covered by Access-Control-Allow-Headers.
7 preflight-header Content-Type is covered by Access-Control-Allow-Headers.
8 preflight-credentials Access-Control-Allow-Credentials: true is present and Allow-Origin is a
concrete origin.
9 preflight-cache Access-Control-Max-Age: 600 — the result is cached per origin, URL, method
and header set. Browsers clamp it to Safari 600s, Chromium 7200s,
Firefox 86400s.
10 actual-request The real PUT request is sent with cookies attached (credentials: "include").
11 actual-response Access-Control-Allow-Origin: https://app.example.com matches. Your code can
read the response body and status.
```
如果将 `Authorization` 改为不在 `-Headers` 中的值,第 6 步就会变为失败,整个解析过程会停在“真实请求从未发送”,最终结论将显示为“在预检阶段被阻止”。
## 输入参数参考
| 控件 | 含义 | 备注 |
| --- | --- | --- |
| 目标技术栈 | 渲染哪个生成器 | 改变语法和注意事项,但不改变策略 |
| 精确列表 | 固定的 origins 集合 | 协议 + 主机 + 可选端口。无路径,无尾部斜杠 |
| 域名后缀 | 主机后缀匹配 | 为每个条目加点号锚定(`.example.com`)。协议需单独指定 |
| 正则表达式 | 任意模式 | 每个生成器都会将其锚定为 `^(?:…)$`;并使用恶意 origins 进行试探 |
| 通配符 | `Access-Control-Allow-Origin: *` | 与凭证不兼容 |
| 协议(后缀模式) | `https` / `http` / 二者皆可 | 除非有特殊原因,否则默认使用 `https` |
| 允许的方法 | `Access-Control-Allow-Methods` | 这里无需包含 `OPTIONS`——它本身就是预检方法 |
| 允许的请求 headers | `Access-Control-Allow-Headers` | 这些是导致请求触发预检的原因 |
| 暴露的响应 headers | `Access-Control-Expose-Headers` | 如果没有此项,安全名单外的所有内容对 JS 都是不可见的 |
| 凭证 | `Access-Control-Allow-Credentials` | 使白名单成为身份验证的边界 |
| 短路 `OPTIONS` | 使用 204 响应预检 | 关闭意味着您的应用必须为 `OPTIONS` 返回 2xx |
| `Vary: Origin` | 缓存正确性 | 对于动态白名单强制开启 |
| `Max-Age` | 预检缓存生命周期 | 上限分别为 600秒、7200秒 和 86400秒 |
Origins 在输入时会进行标准化:主机会被转为小写,默认端口(https 的 `:443`,http 的 `:80`)会被丢弃,因此 `HTTPS://App.Example.COM:443` 和 `https://app.example.com` 被视为同一条目。
## API 接口说明
`src/cors-core.js` 和 `src/generators.js` 可以直接使用,在浏览器中作为全局变量,或者在 Node 环境下作为 CommonJS 模块。所有函数都是纯函数;不执行任何 I/O 操作。
```
const core = require('./src/cors-core.js');
const generators = require('./src/generators.js');
```
### `cors-core.js`
| 函数 | 返回值 |
| --- | --- |
| `normalizeConfig(raw)` | 完整的配置对象:填充默认值,将字符串解析为列表,标准化 origins,将 `maxAge` 限制在 0–31536000 之间 |
| `parseOrigin(value)` | `{scheme, host, port, opaque, serialized}`,如果输入不是有效的序列化 origin 则返回 `null` |
| `buildPlan(config)` | 有效的 header 计划,包含 `dynamic`、`vary`、`credentials` 和 `refusals[]` |
| `resolveAllowOrigin(configOrPlan, origin)` | 服务器应发送的 `Access-Control-Allow-Origin` 值,如果拒绝则返回 `null` |
| `isSafelistedRequestHeader(name, value)` | 判断某个 header 是否可以在不触发预检的情况下发送 |
| `analyze(config)` | `Array<{level, id, title, detail, link?}>`,按 critical → warning → note 排序 |
| `predict(config, request)` | `{preflight, preflightReasons[], preflightPass, steps[], blocked, allowOrigin, summary}` |
| `encodeState(config)` / `decodeState(hash)` | URL 片段序列化;如果片段为空或无法识别,`decodeState` 返回 `null` |
常量也会被导出:`SAFELISTED_REQUEST_HEADERS`、`SAFELISTED_RESPONSE_HEADERS`、`SIMPLE_CONTENT_TYPES`、`SIMPLE_METHODS`、`MAX_AGE_CAPS`、`ORIGIN_MODES`、`DEFAULT_CONFIG`。
### `generators.js`
| 函数 | 返回值 |
| --- | --- |
| `list()` | 每个技术栈的 `[{id, label, family}]` |
| `has(id)` | 判断某个技术栈 id 是否已知 |
| `generate(config, stackId?)` | `{id, label, family, plan, snippets: [{title, language, code, note}], caveats: [string]}` |
当传入未知的 id 时,`generate` 会抛出一个列出所有已知技术栈的 `RangeError`。这是这两个模块中唯一会抛出异常的函数。
这两个模块都不会读取文件系统、网络或 `process.env`,因此将它们嵌入到其他工具中是非常安全的。
## 支持的技术栈及其注意事项
每个技术栈的完整注意事项列表会在应用程序中其生成的配置下方渲染显示。以下是简要概述:
### Nginx
`add_header` 只有在内部块没有声明任何属于自己的 `add_header` 时,才会从外部块继承,因此如果在 `location` 中存在哪怕一个随意的 `add_header`,也会静默删除 `server` 块中的所有 CORS headers。如果不加 `always`,`add_header` 会跳过大多数 4xx 和 5xx 响应,因此错误会以 CORS 失败的形式到达浏览器,而不是它们真实的状态码。如果变量的值为空,Nginx 会忽略该 header,这正是使得“带有空默认值的 `map`”这一用法安全的原因。如果上游也设置了 `Access-Control-Allow-Origin`,您最终会得到两个该 header,浏览器会拒绝整个响应——请使用 `proxy_hide_header` 剥离其中一个。
延伸阅读:[在 Nginx 中为多个 origins 配置 CORS](https://www.cross-origin.com/server-side-cors-configuration-header-management/dynamic-origin-validation-patterns/configuring-cors-in-nginx-for-multiple-origins/)
以及 [Nginx 上游预检委托](https://www.cross-origin.com/preflight-request-optimization-caching-strategies/proxy-bypass-strategies/nginx-upstream-preflight-delegation/)。
### Apache httpd
不带 `always` 的 `Header set` 会在错误响应中被丢弃。`.htaccess` 或 `` 块中的 `Header` 会在封闭作用域之后运行,且 `set` 会替换而不是追加,这就是为什么 `Vary` 要使用 `append`。`SetEnvIf` 匹配的是原始的 `Origin` header,因此模式必须保持锚定。需要 `mod_headers`,而 `OPTIONS` 短路则需要 `mod_rewrite`。
### Express
必须在 body 解析、会话和身份验证之前挂载——预检没有 cookies 也没有 body,因此任何检查这些内容的中间件都会优先拒绝它。`app.use` 的路径是前缀匹配的。生成器会输出动态 origin 回调,绝不会使用 `origin: true`。一个不带 `Access-Control-Request-Method` 的裸 `OPTIONS` 请求不是预检,允许直接放行。
延伸阅读:[Express 动态 origin 白名单实现](https://www.cross-origin.com/server-side-cors-configuration-header-management/dynamic-origin-validation-patterns/expressjs-dynamic-origin-allowlist-implementation/)。
### Fastify
插件默认是封装隔离的;请在根实例上注册或使用 `fastify-plugin` 包装。`onRequest` 钩子在路由和 body 解析之前运行,这正是预检短路应该放置的位置——否则 Fastify 会对未路由的 `OPTIONS` 返回 404,在任何 header 被读取之前导致预检失败。从钩子中返回回复正是停止生命周期的方法。
### FastAPI / Starlette
Starlette 使用 **200** 而不是 204 来响应预检——这依然是一个有效的 2xx 响应。将 `allow_origins=["*"]` 与 `allow_credentials=True` 结合使用,会使 Starlette 静默地直接反射请求 origin,这等同于带凭证的盲目反射;生成器绝不会输出这种组合。请最后添加 `CORSMiddleware`,使其包装包括身份验证中间件在内的所有内容。
### Django
`CommonMiddleware` 的 `APPEND_SLASH` 和 `SecurityMiddleware` 的 `SECURE_SSL_REDIRECT` 都会产生 301 重定向,而预检如果收到 301 就会立即失败——因此它被放置在靠近 `MIDDLEWARE` 顶部的位置。CSRF 保护与 CORS 无关,但仍然适用;请将您允许的 origins 也添加到 `CSRF_TRUSTED_ORIGINS` 中。`patch_vary_headers` 是合并而不是替换。生成的中间件是纯粹的 Django 代码,不依赖任何第三方包。
### Flask
在调试模式下,如果发生未处理的异常,`after_request` 将不会运行;请注册一个 `errorhandler` 确保其运行,否则 500 错误会以 CORS 错误的形式到达浏览器。Flask 自动生成的 `OPTIONS` 处理程序仅覆盖已存在的路由,因此 `before_request` 短路机制也涵盖了未知路径。
### Spring Boot
如果 classpath 中存在 Spring Security,过滤器链会在 `DispatcherServlet` 之前运行,并且除非启用了 `http.cors()` 并且存在 `CorsConfigurationSource` bean,否则会对无凭证的预检返回 401——这是最常见的 Spring CORS 失败原因,因此生成器将此 bean 作为第三段代码片段输出。在 `setAllowedOrigins("*")` 的同时调用 `setAllowCredentials(true)` 会在启动时抛出异常。`allowedOriginPatterns` 是 Spring 的专有模式,而不是正则表达式:`*` 仅在单个组件内匹配。控制器上的 `@CrossOrigin` 注解是替换全局映射,而不是与其合并。
### ASP.NET Core
同时使用 `AllowAnyOrigin()` 和 `AllowCredentials()` 会在启动时抛出异常。`SetIsOriginAllowed(_ => true)` 是盲目反射,并且*确实*与凭证兼容,这使其比通配符更危险;它在许多示例代码中都出现过。`UseCors` 必须位于 `UseRouting` 之后和 `UseAuthorization` 之前。对于任何非通配符策略,框架自动发送 `Vary: Origin`。
### Go net/http
Headers 必须在第一次调用 `Write` 或 `WriteHeader` 之前完全设置好;在此之后设置是无效的静默操作。`Header.Set` 是替换,`Header.Add` 是追加——`Vary` 使用 `Add`。Go 1.22 的 `ServeMux` 模式会匹配方法,因此 `POST /path` 模式不会匹配到 `OPTIONS` 预检;因此短路逻辑被放置在 mux 之外。
### Ruby on Rails (Rack)
插入在位置 0,因为 `ActionDispatch::HostAuthorization` (403) 和 `ActionDispatch::SSL` (301) 都会较早运行,且都会导致预检失败。Rails 的 CSRF 保护独立于 CORS。在 Rack 3 中,header 名称必须是小写。生成的代码是纯粹的 Rack 中间件,不需要任何 gem。
### Cloudflare Worker
来自 `fetch()` 的 `Response` 拥有不可变的 headers;请根据其 body 构建一个新的 Response。使用 `headers.set`,千万不要使用 `append`,否则您会生成两个 `Access-Control-Allow-Origin` 值,浏览器会拒绝该响应。从历史上看,Cloudflare 的边缘缓存会忽略除 `Accept-Encoding` 之外的任何 `Vary` 字段,因此在那里不要依赖它。仪表板的 Transform Rules 会独立于 Worker 添加 headers。
延伸阅读:[Cloudflare Workers CORS 预检处理](https://www.cross-origin.com/preflight-request-optimization-caching-strategies/proxy-bypass-strategies/cloudflare-workers-cors-preflight-handling/)
以及[调试 Cloudflare 背后的 CORS 失败](https://www.cross-origin.com/cross-origin-debugging-error-diagnosis/proxy-layer-cors-troubleshooting/debugging-cors-failures-behind-cloudflare/)。
### AWS API Gateway
HTTP APIs (v2) 具有一流的 CORS 配置,无需调用您的集成即可响应预检;REST APIs (v1) 则没有,需要为每个资源配置一个带有 MOCK 集成的 `OPTIONS` 方法,并在网关响应中为 4XX 和 5XX 添加 CORS headers。`AllowOrigins` 支持单个主机通配符,但不支持正则表达式。在配置时就会拒绝同时使用 `AllowCredentials` 和 `["*"]`。网关的 CORS 配置和集成发出的 headers 不会合并,因此请选择其中一层来处理。
延伸阅读:[AWS API Gateway CORS 预检配置](https://www.cross-origin.com/preflight-request-optimization-caching-strategies/proxy-bypass-strategies/aws-api-gateway-cors-preflight-configuration/)。
### Caddy 2
如果不使用 `defer`,`header` 指令会在反向代理写入其响应之前运行,并且同名的上游 header 会被追加而不是替换——从而产生重复项。`header` 匹配器是精确比较 `Origin`;`header_regexp` 使用的是 RE2,因此不支持前瞻或反向引用。指令顺序由 Caddy 固定,而不是由您编写它们的顺序决定。
### Envoy
`cors` 过滤器必须出现在路由器之前的 `http_filters` 中,否则策略将静默失效。`safe_regex` 使用的是 RE2。Envoy 使用 200 响应预检,并且*不会*添加 `Vary: Origin`——如果前方存在共享缓存,请通过 `response_headers_to_add` 手动添加。路由级别的策略会覆盖虚拟主机级别的策略,而不是合并。`typed_per_filter_config` 替换了已弃用的路由级 `cors` 字段。
## 测试
```
node tests/run-tests.js
```
190 个测试用例,仅使用 `node:test` 和 `node:assert`。它们涵盖了:
- 配置标准化、origin 解析以及对格式错误 origins 的拒绝;
计划,包括所有的拒绝规则;
- 所有四种 origin 模式的白名单匹配,包括恶意输入测试
(`https://example.com.evil.com`、`https://evilexample.com`、`null`、无法编译的正则表达式);
- 每一个安全审查的发现结果,包括危险级别的升级情况;
- 跨越多种组合的预检触发与结果,`Authorization` 与 `*` 的规则对比,带凭证的通配符规则,以及暴露 headers 的规则;
- URL 片段的双向转换解析;
- 一个由六种配置在所有十五种技术栈上交叉组成的矩阵测试,用于断言没有渲染异常、任何输出中都不包含通配符加凭证的组合、在技术栈支持的地方包含 `Vary: Origin`、在技术栈本身不响应预检的地方进行 204 短路处理,并且没有任何生成器在没有前置检查的情况下反射 `Origin`;
- 针对特定技术栈的预期测试:每个 Nginx 的 `add_header` 都带有 `always`、Express 输出中不包含 `origin: true`、Spring 输出全部三种形式、API Gateway 的 JSON 能够解析、Envoy 的正则表达式位于单引号的 YAML 标量中、Rails 的正则表达式使用 `\A`/`\z` 锚定,等等。
退出代码 0 表示所有测试通过,1 表示至少有一个测试失败。
## 局限性以及刻意不做的事情
- **它不会探测任何东西。** 该代码库中没有任何网络请求代码。它绝不会向您的 API 发送请求,也无法告诉您服务器目前的行为是什么——只能展示它生成的配置将会产生什么效果。验证部署后的实际状态是另一项独立的工作,最好使用 `curl -X OPTIONS` 和 DevTools 来完成。
- **它不验证技术栈的语法。** 生成的 Nginx 配置不会被 Nginx 解析,生成的 Java 代码也不会被编译。这些代码片段是已知正确结构并插入了您的变量值的模板;在部署之前,请运行 `nginx -t`、`caddy validate` 或您的构建命令进行检查。
- **它生成的是策略,而不是授权模型。** CORS 是同源策略在浏览器层面上的一种放宽机制。它无法阻止 `curl`、服务器端代理或移动应用发起的请求。如果必须拒绝某个请求,请使用身份验证和授权机制;CORS 策略仅仅控制哪些*网页*可以读取响应。
- **它不是防范 CSRF 的控制手段。** 一个过于宽松的 CORS 策略可能会促成类似于 CSRF 的攻击,但严格的 CORS 策略并不能阻止 CSRF:一个简单的跨域 `POST` 请求依然会被发送,即使随后的响应被阻止,也无法撤销已经执行的写入操作。
- **正则表达式探测是启发式的。** 通过这些测试仅仅意味着那三个已知的恶意探测失败了,并不代表该模式绝对安全。锚定和转义依然需要人工审查。
- **Private Network Access、COOP/COEP 以及 `Sec-Fetch-*`** 不在讨论范围内。它们与跨域请求有交集,但属于拥有独立 headers 的另一种机制。
- **浏览器预览模拟的是标准,而不是特定的浏览器版本。** 各大浏览器在 CORS 协议上高度一致,但在 `Max-Age` 上限、`null` origin 的边缘情况以及客户端提示的安全名单方面确实存在差异。
## 延伸阅读
有关本工具所实现机制的背景资料,节选自
[cross-origin.com](https://www.cross-origin.com/),这是一个关于 CORS、预检机制和浏览器安全边界的技术参考网站:
- [同源策略基础](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/)
- [简单请求与预检请求](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/simple-vs-preflight-requests/)
- [为什么预检请求使用 OPTIONS 方法](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/simple-vs-preflight-requests/why-preflight-requests-use-options-method/)
- [Origin 匹配规则与验证](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/origin-matching-rules-validation/)
- [为什么 localhost 和 127.0.0.1 是不同的 origins](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/origin-matching-rules-validation/why-localhost-and-127-0-0-1-are-different-origins/)
- [深入理解 Access-Control-Allow-Credentials](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/credential-sharing-security-boundaries/understanding-access-control-allow-credentials/)
- [正确处理 Vary: Origin](https://www.cross-origin.com/server-side-cors-configuration-header-management/access-control-header-directives/handling-vary-origin-header-correctly/)
- [通配符与动态 origin 反射:何时使用哪种](https://www.cross-origin.com/server-side-cors-configuration-header-management/cors-security-auditing/wildcard-vs-dynamic-origin-reflection-when-to-use-each/)
- [CORS 安全审计清单](https://www.cross-origin.com/server-side-cors-configuration-header-management/cors-security-auditing/cors-security-audit-checklist/)
- [在开发中安全地允许 localhost origins](https://www.cross-origin.com/server-side-cors-configuration-header-management/wildcard-risks-mitigation/safely-allowing-localhost-origins-in-development/)
- [如何有效设置 Access-Control-Max-Age](https://www.cross-origin.com/preflight-request-optimization-caching-strategies/cache-duration-tuning-max-age/how-to-set-access-control-max-age-effectively/)
- [设计轻量级的 OPTIONS 端点](https://www.cross-origin.com/preflight-request-optimization-caching-strategies/options-endpoint-design/designing-lightweight-options-endpoints/)
- [通过 header 缓存降低预检频率](https://www.cross-origin.com/preflight-request-optimization-caching-strategies/header-deduplication-techniques/reducing-preflight-frequency-with-header-caching/)
- [在 DevTools 中解读预检 OPTIONS 请求](https://www.cross-origin.com/cross-origin-debugging-error-diagnosis/devtools-network-preflight-inspection/reading-preflight-options-request-in-devtools/)
- [使用 curl OPTIONS 模拟预检请求](https://www.cross-origin.com/cross-origin-debugging-error-diagnosis/curl-cors-reproduction/simulating-preflight-with-curl-options/)
- [修复重复的 Access-Control-Allow-Origin header](https://www.cross-origin.com/cross-origin-debugging-error-diagnosis/proxy-layer-cors-troubleshooting/fixing-duplicate-access-control-allow-origin-header/)
- [调试缺失的 Access-Control-Allow-Origin header](https://www.cross-origin.com/core-cors-mechanics-same-origin-policy-fundamentals/cors-error-code-breakdown/debugging-missing-access-control-allow-origin-header/)
- [SameSite=None 与 CORS 凭证的权衡](https://www.cross-origin.com/server-side-cors-configuration-header-management/credential-sync-across-subdomains/samesitenone-vs-cors-credentials-tradeoffs/)
## 贡献
请查阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 许可证
MIT。请查阅 [LICENSE](LICENSE)。
标签:API安全审查, CORS, MITM代理, SOC Prime, Syscall, Web开发, 代码生成, 前端, 多模态安全, 开发工具, 数据可视化, 渗透测试工具, 自定义脚本, 配置生成器