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开发, 无后门, 网络安全, 逆向工具, 隐私保护