Skip to main content

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:

  1. Fetch Metadata: Sec-Fetch-Site of same-origin, same-site, or none is allowed; cross-site is rejected.
  2. Origin header: compared against the request's own origin and any allowed_origins. Origin: null is rejected.
  3. Neither header present: allowed unless allow_unverifiable_requests is 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

asgi_cross_origin_protection-0.1.0.tar.gz (7.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

asgi_cross_origin_protection-0.1.0-py3-none-any.whl (10.2 kB view details)

Uploaded Python 3

File details

Details for the file asgi_cross_origin_protection-0.1.0.tar.gz.

File metadata

File hashes

Hashes for asgi_cross_origin_protection-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f17d89160a0e145aa0e5e9464c58a23c2e3f7d17b44984f3f08947742bb3e693
MD5 837876fffc4de129118d85139278aaf7
BLAKE2b-256 f7d8187b303ae0da39df8bebc43caf4b73b6cb287098306499263bb533f16af1

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file asgi_cross_origin_protection-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for asgi_cross_origin_protection-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 72996ab6bdcb909411eb38a6b9c708ab4e451983b5c670fec58a71222ee27318
MD5 14145f7feff81f48d97e357c62349bfb
BLAKE2b-256 1d6325c038cf9967877842ed88aa41f725b856363c774a189281beca3e14da21

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page