{"id":51826473,"url":"https://github.com/thomasdesr/asgi-cross-origin-protection","last_synced_at":"2026-07-22T10:33:39.848Z","repository":{"id":365310678,"uuid":"1268826540","full_name":"thomasdesr/asgi-cross-origin-protection","owner":"thomasdesr","description":"Drop-in, token-free CSRF protection for any ASGI app, using Fetch Metadata. No dependencies.","archived":false,"fork":false,"pushed_at":"2026-06-16T19:06:18.000Z","size":57,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-16T21:08:12.775Z","etag":null,"topics":["asgi","cross-origin","csrf","fastapi","fetch-metadata","middleware","security","starlette"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thomasdesr.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-14T01:31:49.000Z","updated_at":"2026-06-16T19:08:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/thomasdesr/asgi-cross-origin-protection","commit_stats":null,"previous_names":["thomasdesr/asgi-cross-origin-protection"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/thomasdesr/asgi-cross-origin-protection","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasdesr%2Fasgi-cross-origin-protection","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasdesr%2Fasgi-cross-origin-protection/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasdesr%2Fasgi-cross-origin-protection/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasdesr%2Fasgi-cross-origin-protection/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thomasdesr","download_url":"https://codeload.github.com/thomasdesr/asgi-cross-origin-protection/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thomasdesr%2Fasgi-cross-origin-protection/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35759166,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-22T02:00:06.236Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asgi","cross-origin","csrf","fastapi","fetch-metadata","middleware","security","starlette"],"created_at":"2026-07-22T10:33:39.189Z","updated_at":"2026-07-22T10:33:39.834Z","avatar_url":"https://github.com/thomasdesr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# asgi-cross-origin-protection\n\nCross-origin request protection ASGI middleware. It rejects cross-site\nstate-changing requests (CSRF defense) by inspecting Fetch Metadata headers,\nwith an Origin fallback. It needs no CSRF tokens or session state. The defaults\nare safe for most apps without configuration.\n\nPure ASGI with no dependencies: works with Starlette, FastAPI,\nLitestar, Quart, Django-ASGI, or any other ASGI app.\n\n## Install\n\n```bash\nuv add asgi-cross-origin-protection\n```\n\n## Usage\n\nWrap your app. For most apps this is all you need:\n\n```python\nfrom asgi_cross_origin_protection import CrossOriginProtection\n\napp = CrossOriginProtection(app)\n```\n\nWith Starlette/FastAPI's `add_middleware`:\n\n```python\nfrom fastapi import FastAPI\nfrom asgi_cross_origin_protection import CrossOriginProtection\n\napp = FastAPI()\napp.add_middleware(CrossOriginProtection)\n```\n\nThe default policy rejects cross-site requests that change state, while\nallowing same-origin requests, non-browser clients (mobile apps, CLIs,\nserver-to-server), and inbound links. A cross-site attacker cannot forge the\n`Sec-Fetch-Site` header or strip `Origin` from a browser request, so the CSRF\nvector is still closed.\n\n## When to change the defaults\n\nYou only need to touch configuration if one of these applies:\n\n| If your app… | Set |\n|--------------|-----|\n| trusts specific partner origins | `allowed_origins=(\"https://partner.example\",)` |\n| has paths that must skip the check (health probes, webhooks) | `exempt_paths=(\"/healthz\",)` |\n| should return something other than the default 403 JSON | `deny_app=...` (see below) |\n\n```python\napp = CrossOriginProtection(\n    app,\n    allowed_origins=(\"https://app.example.com\",),\n    exempt_paths=(\"/healthz\",),\n)\n```\n\n`allowed_origins` entries must be bare origins (`scheme://host[:port]`); an\nentry with a path, query, fragment, or missing scheme/host raises a `ValueError`\nat construction. `exempt_paths` entries must be absolute (start with `/`) and\nmatch on path-segment boundaries, so `/healthz` exempts `/healthz` and\n`/healthz/live` but not `/healthz-internal`; a non-absolute or empty entry\nraises a `ValueError`. An exemption applies to every method (in practice only\nstate-changing ones, since safe methods are always allowed regardless).\n\n### Custom rejection response\n\n`deny_app` is any ASGI app. Starlette/FastAPI `Response` instances are\nthemselves ASGI apps, so you can pass one directly:\n\n```python\nfrom starlette.responses import PlainTextResponse\n\napp = CrossOriginProtection(\n    app,\n    deny_app=PlainTextResponse(\"forbidden\", status_code=403),\n)\n```\n\n## How it decides\n\nA request is evaluated in this order; the first conclusive signal wins:\n\n1. **`allowed_origins`**: an `Origin` in this set is allowed regardless of the\n   signals below, so a trusted partner's cross-site request still passes.\n2. **Fetch Metadata**: only `Sec-Fetch-Site` of `same-origin` or `none` is\n   allowed; `same-site`, `cross-site`, and any unrecognized value are rejected.\n   A present `Sec-Fetch-Site` is conclusive — the Origin step below is skipped.\n3. **Origin header**: compared against the request's own host. The comparison\n   is scheme-blind (the request's scheme is unreliable behind a TLS-terminating\n   proxy; relies on HSTS, as Go does). `Origin: null` is rejected.\n4. **Neither header present** (or an empty `Origin`): allowed unless\n   `allow_unverifiable_requests` is cleared.\n\nSafe methods (GET/HEAD/OPTIONS) are always allowed; rejection applies to\nstate-changing methods.\n\n### Hardening\n\n`allow_unverifiable_requests` (default `True`) governs requests that carry\nneither `Sec-Fetch-Site` nor `Origin`, so their origin cannot be checked. These\nare typically non-browser clients (mobile apps, CLIs, server-to-server). They\nare allowed by default because a browser CSRF attempt always carries one of\nthose headers. Set it to `False` only if your app serves browsers exclusively\nand you want to reject everything else:\n\n```python\napp = CrossOriginProtection(app, allow_unverifiable_requests=False)\n```\n\n## Cross-origin isolation headers\n\nCOOP/COEP/CORP isolation headers are a separate, optional middleware. Most apps\ndo not need them. Reach for `CrossOriginIsolation` when you specifically want\ncross-origin isolation, for example to enable `crossOriginIsolated` and APIs\nlike `SharedArrayBuffer`:\n\n```python\nfrom asgi_cross_origin_protection.isolation import CrossOriginIsolation\n\napp = CrossOriginIsolation(app)\n```\n\nEach policy is added only when the wrapped app did not already set that header.\nPass `None` for a policy to leave its header alone. Defaults: COOP `same-origin`,\nCOEP `require-corp`, CORP `same-site`.\n\n`require-corp` is a breaking default: once a document carries it, every\ncross-origin subresource it loads (CDN scripts, images, fonts) must itself send\n`Cross-Origin-Resource-Policy` or CORS headers, or the browser blocks it. That is\ninherent to cross-origin isolation — pass `embedder_policy=None` for COOP and CORP\nwithout it.\n\nCompose both middlewares when you want protection and isolation together.\n\n## Development\n\n```bash\nmake dev     # sync dependencies and install the prek git hook\nmake lint    # run all checks via prek (ruff, ty, zizmor)\nmake test    # pytest (100% coverage gate)\n```\n\nThe same prek hooks run automatically on every commit; `make lint` runs them\nacross all files on demand.\n\n## Influences\n\n- Go's [`net/http.CrossOriginProtection`](https://pkg.go.dev/net/http#CrossOriginProtection),\n  whose API and safe-by-default policy this package mirrors.\n- Filippo Valsorda's [Cross-Site Request Forgery](https://words.filippo.io/csrf/),\n  the reasoning behind that design.\n- [XS-Leaks Wiki](https://xsleaks.dev/), background on the cross-site leak\n  classes the isolation headers help defend against.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasdesr%2Fasgi-cross-origin-protection","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthomasdesr%2Fasgi-cross-origin-protection","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthomasdesr%2Fasgi-cross-origin-protection/lists"}