Drop-in, token-free CSRF protection for any ASGI app, using Fetch Metadata. No dependencies.
Project description
asgi-cross-origin-protection
Cross-origin request protection ASGI middleware. It rejects cross-site state-changing requests (CSRF defense) by inspecting Fetch Metadata headers, with an Origin fallback. It needs no CSRF tokens or session state. The defaults are safe for most apps without configuration.
Pure ASGI with no dependencies: works with Starlette, FastAPI, Litestar, Quart, Django-ASGI, or any other ASGI app.
Install
uv add asgi-cross-origin-protection
Usage
Wrap your app. For most apps this is all you need:
from asgi_cross_origin_protection import CrossOriginProtection
app = CrossOriginProtection(app)
With Starlette/FastAPI's add_middleware:
from fastapi import FastAPI
from asgi_cross_origin_protection import CrossOriginProtection
app = FastAPI()
app.add_middleware(CrossOriginProtection)
The default policy rejects cross-site requests that change state, while
allowing same-origin requests, non-browser clients (mobile apps, CLIs,
server-to-server), and inbound links. A cross-site attacker cannot forge the
Sec-Fetch-Site header or strip Origin from a browser request, so the CSRF
vector is still closed.
When to change the defaults
You only need to touch configuration if one of these applies:
| If your app… | Set |
|---|---|
| trusts specific partner origins | allowed_origins=("https://partner.example",) |
| has paths that must skip the check (health probes, webhooks) | exempt_paths=("/healthz",) |
| should return something other than the default 403 JSON | deny_app=... (see below) |
app = CrossOriginProtection(
app,
allowed_origins=("https://app.example.com",),
exempt_paths=("/healthz",),
)
Custom rejection response
deny_app is any ASGI app. Starlette/FastAPI Response instances are
themselves ASGI apps, so you can pass one directly:
from starlette.responses import PlainTextResponse
app = CrossOriginProtection(
app,
deny_app=PlainTextResponse("forbidden", status_code=403),
)
How it decides
A request is evaluated in this order; the first conclusive signal wins:
- Fetch Metadata:
Sec-Fetch-Siteofsame-origin,same-site, ornoneis allowed;cross-siteis rejected. - Origin header: compared against the request's own origin and any
allowed_origins.Origin: nullis rejected. - Neither header present: allowed unless
allow_unverifiable_requestsis cleared.
Safe methods (GET/HEAD/OPTIONS/TRACE) are always allowed; rejection applies to state-changing methods.
Hardening
allow_unverifiable_requests (default True) governs requests that carry
neither Sec-Fetch-Site nor Origin, so their origin cannot be checked. These
are typically non-browser clients (mobile apps, CLIs, server-to-server). They
are allowed by default because a browser CSRF attempt always carries one of
those headers. Set it to False only if your app serves browsers exclusively
and you want to reject everything else:
app = CrossOriginProtection(app, allow_unverifiable_requests=False)
Cross-origin isolation headers
COOP/COEP/CORP isolation headers are a separate, optional middleware. Most apps
do not need them. Reach for CrossOriginIsolation when you specifically want
cross-origin isolation, for example to enable crossOriginIsolated and APIs
like SharedArrayBuffer:
from asgi_cross_origin_protection.isolation import CrossOriginIsolation
app = CrossOriginIsolation(app)
Each policy is added only when the wrapped app did not already set that header.
Pass None for a policy to leave its header alone. Defaults: COOP same-origin,
COEP require-corp, CORP same-site.
Compose both middlewares when you want protection and isolation together.
Development
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)
The same prek hooks run automatically on every commit; make lint runs them
across all files on demand.
Influences
- Go's
net/http.CrossOriginProtection, whose API and safe-by-default policy this package mirrors. - Filippo Valsorda's Cross-Site Request Forgery, the reasoning behind that design.
- XS-Leaks Wiki, background on the cross-site leak classes the isolation headers help defend against.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file asgi_cross_origin_protection-0.1.0.tar.gz.
File metadata
- Download URL: asgi_cross_origin_protection-0.1.0.tar.gz
- Upload date:
- Size: 7.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f17d89160a0e145aa0e5e9464c58a23c2e3f7d17b44984f3f08947742bb3e693
|
|
| MD5 |
837876fffc4de129118d85139278aaf7
|
|
| BLAKE2b-256 |
f7d8187b303ae0da39df8bebc43caf4b73b6cb287098306499263bb533f16af1
|
Provenance
The following attestation bundles were made for asgi_cross_origin_protection-0.1.0.tar.gz:
Publisher:
publish.yml on thomasdesr/asgi-cross-origin-protection
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
asgi_cross_origin_protection-0.1.0.tar.gz -
Subject digest:
f17d89160a0e145aa0e5e9464c58a23c2e3f7d17b44984f3f08947742bb3e693 - Sigstore transparency entry: 1840920891
- Sigstore integration time:
-
Permalink:
thomasdesr/asgi-cross-origin-protection@553d61bbfcdb344a682a7796fad46db3bf1ba395 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/thomasdesr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@553d61bbfcdb344a682a7796fad46db3bf1ba395 -
Trigger Event:
release
-
Statement type:
File details
Details for the file asgi_cross_origin_protection-0.1.0-py3-none-any.whl.
File metadata
- Download URL: asgi_cross_origin_protection-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
72996ab6bdcb909411eb38a6b9c708ab4e451983b5c670fec58a71222ee27318
|
|
| MD5 |
14145f7feff81f48d97e357c62349bfb
|
|
| BLAKE2b-256 |
1d6325c038cf9967877842ed88aa41f725b856363c774a189281beca3e14da21
|
Provenance
The following attestation bundles were made for asgi_cross_origin_protection-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on thomasdesr/asgi-cross-origin-protection
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
asgi_cross_origin_protection-0.1.0-py3-none-any.whl -
Subject digest:
72996ab6bdcb909411eb38a6b9c708ab4e451983b5c670fec58a71222ee27318 - Sigstore transparency entry: 1840920960
- Sigstore integration time:
-
Permalink:
thomasdesr/asgi-cross-origin-protection@553d61bbfcdb344a682a7796fad46db3bf1ba395 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/thomasdesr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@553d61bbfcdb344a682a7796fad46db3bf1ba395 -
Trigger Event:
release
-
Statement type: