thomasdesr/asgi-cross-origin-protection
GitHub: thomasdesr/asgi-cross-origin-protection
一个纯 ASGI 实现的无依赖 CSRF 防护中间件,通过 Fetch Metadata 标头和 Origin 回退机制在不使用 token 的情况下阻止跨站状态变更请求。
Stars: 0 | Forks: 0
# asgi-cross-origin-protection
跨域请求保护 ASGI 中间件。它通过检查 Fetch Metadata 标头并结合 Origin 回退机制,拒绝跨站的状态更改请求(CSRF 防御)。它不需要 CSRF token 或 session 状态。对于大多数应用程序来说,默认配置无需额外设置即是安全的。
纯 ASGI 实现,无依赖:兼容 Starlette、FastAPI、Litestar、Quart、Django-ASGI 或任何其他 ASGI 应用程序。
## 安装
```
uv add asgi-cross-origin-protection
```
## 用法
包装你的应用程序。对于大多数应用程序,你只需这样做:
```
from asgi_cross_origin_protection import CrossOriginProtection
app = CrossOriginProtection(app)
```
结合 Starlette/FastAPI 的 `add_middleware`:
```
from fastapi import FastAPI
from asgi_cross_origin_protection import CrossOriginProtection
app = FastAPI()
app.add_middleware(CrossOriginProtection)
```
默认策略会拒绝更改状态的跨站请求,同时允许同源请求、非浏览器客户端(移动应用、CLI、服务器到服务器)以及入站链接。跨站攻击者无法伪造 `Sec-Fetch-Site` 标头或从浏览器请求中剥离 `Origin`,因此 CSRF 攻击向量依然被封闭。
## 何时更改默认设置
只有当满足以下条件之一时,你才需要调整配置:
| 如果你的应用… | 设置 |
|--------------|-----|
| 信任特定的合作伙伴源 | `allowed_origins=("https://partner.example",)` |
| 有需要跳过检查的路径(健康探针、webhook) | `exempt_paths=("/healthz",)` |
| 应该返回默认 403 JSON 以外的内容 | `deny_app=...`(见下文) |
```
app = CrossOriginProtection(
app,
allowed_origins=("https://app.example.com",),
exempt_paths=("/healthz",),
)
```
`allowed_origins` 的条目必须是纯源(`scheme://host[:port]`);包含路径、查询、片段或缺少 scheme/host 的条目在构建时会引发 `ValueError`。`exempt_paths` 的条目必须是绝对路径(以 `/` 开头),并在路径段边界上进行匹配,因此 `/healthz` 会豁免 `/healthz` 和 `/healthz/live`,但不会豁免 `/healthz-internal`;非绝对或空的条目会引发 `ValueError`。豁免适用于所有方法(实际上仅限于状态更改的方法,因为安全方法无论如何总是被允许的)。
### 自定义拒绝响应
`deny_app` 可以是任何 ASGI 应用程序。Starlette/FastAPI 的 `Response` 实例本身就是 ASGI 应用程序,因此你可以直接传递一个实例:
```
from starlette.responses import PlainTextResponse
app = CrossOriginProtection(
app,
deny_app=PlainTextResponse("forbidden", status_code=403),
)
```
## 它是如何做决策的
请求按以下顺序进行评估;第一个确切的信号优先:
1. **`allowed_origins`**:无论以下信号如何,此集合中的 `Origin` 都会被允许,因此受信任合作伙伴的跨站请求仍然可以通过。
2. **Fetch Metadata**:仅允许 `same-origin` 或 `none` 的 `Sec-Fetch-Site`;拒绝 `same-site`、`cross-site` 以及任何无法识别的值。存在的 `Sec-Fetch-Site` 是确切信号——将跳过下方的 Origin 步骤。
3. **Origin 标头**:与请求自身的主机进行比较。此比较不区分 scheme(在终止 TLS 的代理后面,请求的 scheme 是不可靠的;像 Go 一样依赖 HSTS)。`Origin: null` 会被拒绝。
4. **两者都不存在**(或 `Origin` 为空):除非清除了 `allow_unverifiable_requests`,否则将被允许。
安全方法(GET/HEAD/OPTIONS)总是被允许的;拒绝仅适用于状态更改方法。
### 强化
`allow_unverifiable_requests`(默认为 `True`)控制既不包含 `Sec-Fetch-Site` 也不包含 `Origin` 的请求,因此无法检查其来源。这些通常是非浏览器客户端(移动应用、CLI、服务器到服务器)。默认情况下它们是被允许的,因为浏览器的 CSRF 尝试总是会携带这些标头之一。仅当你的应用程序专门为浏览器提供服务,并且你希望拒绝所有其他请求时,才将其设置为 `False`:
```
app = CrossOriginProtection(app, allow_unverifiable_requests=False)
```
## 跨域隔离标头
COOP/COEP/CORP 隔离标头是一个独立的可选中间件。大多数应用程序不需要它们。当你明确需要跨域隔离时,请使用 `CrossOriginIsolation`,例如为了启用 `crossOriginIsolated` 和 `SharedArrayBuffer` 等 API:
```
from asgi_cross_origin_protection.isolation import CrossOriginIsolation
app = CrossOriginIsolation(app)
```
每个策略仅在被包装的应用程序尚未设置该标头时才会添加。为策略传入 `None` 可保持其标头不变。默认值:COOP 为 `same-origin`,COEP 为 `require-corp`,CORP 为 `same-site`。
`require-corp` 是一个破坏性的默认设置:一旦某个文档带有此设置,它加载的每个跨域子资源(CDN 脚本、图像、字体)本身都必须发送 `Cross-Origin-Resource-Policy` 或 CORS 标头,否则浏览器将阻止它。这是跨域隔离所固有的——如果只需要 COOP 和 CORP,请传入 `embedder_policy=None`。
当你希望同时获得保护和隔离时,可以组合使用这两个中间件。
## 开发
```
make dev # sync dependencies and install the prek git hook
make lint # run all checks via prek (ruff, ty, zizmor)
make test # pytest (100% coverage gate)
```
相同的 prek 钩子会在每次提交时自动运行;`make lint` 会按需对所有文件运行它们。
## 影响
- Go 的 [`net/http.CrossOriginProtection`](https://pkg.go.dev/net/http#CrossOriginProtection),
本软件包借鉴了它的 API 和安全默认策略。
- Filippo Valsorda 的 [跨站请求伪造](https://words.filippo.io/csrf/),
该设计背后的推理过程。
- [XS-Leaks Wiki](https://xsleaks.dev/),关于隔离标头有助于防御的跨站泄漏类别的背景知识。
标签:ASGI中间件, CSRF防护, Fetch Metadata, Python, Syscall, Web开发, 无后门, 网络安全, 逆向工具, 隐私保护