{"id":50405976,"url":"https://github.com/dmptrluke/django-fetch-metadata","last_synced_at":"2026-05-31T01:30:59.522Z","repository":{"id":347668490,"uuid":"1194846134","full_name":"dmptrluke/django-fetch-metadata","owner":"dmptrluke","description":"Resource isolation policy for Django using Fetch Metadata request headers","archived":false,"fork":false,"pushed_at":"2026-03-28T23:01:21.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-29T00:58:42.466Z","etag":null,"topics":[],"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/dmptrluke.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-03-28T22:16:10.000Z","updated_at":"2026-03-28T23:01:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dmptrluke/django-fetch-metadata","commit_stats":null,"previous_names":["dmptrluke/django-fetch-metadata"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/dmptrluke/django-fetch-metadata","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmptrluke%2Fdjango-fetch-metadata","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmptrluke%2Fdjango-fetch-metadata/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmptrluke%2Fdjango-fetch-metadata/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmptrluke%2Fdjango-fetch-metadata/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dmptrluke","download_url":"https://codeload.github.com/dmptrluke/django-fetch-metadata/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dmptrluke%2Fdjango-fetch-metadata/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33716338,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"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":[],"created_at":"2026-05-31T01:30:57.223Z","updated_at":"2026-05-31T01:30:59.506Z","avatar_url":"https://github.com/dmptrluke.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# django-fetch-metadata\n\nResource isolation policy for Django using\n[Fetch Metadata](https://web.dev/articles/fetch-metadata) request headers.\n\nBrowsers send `Sec-Fetch-Site` and `Sec-Fetch-Mode` headers on every request,\nindicating where the request came from and how it was initiated. This middleware\nuses those headers to block cross-site attacks while allowing legitimate\nsame-origin requests and direct navigations.\n\nThis is a defense-in-depth layer that works alongside Django's CSRF middleware,\nnot a replacement for it. Non-browser clients that don't send Fetch Metadata\nheaders (curl, API consumers, webhooks) pass through by default.\n\n## Installation\n\n```bash\npip install django-fetch-metadata\n```\n\nAdd the middleware to your `MIDDLEWARE` setting, before `CsrfViewMiddleware`:\n\n```python\nMIDDLEWARE = [\n    'django.middleware.common.CommonMiddleware',\n    'fetch_metadata.middleware.FetchMetadataMiddleware',\n    'django.middleware.csrf.CsrfViewMiddleware',\n    # ...\n]\n```\n\nThe DEFAULT preset is opinionated: it blocks all cross-site requests except\nlink clicks (navigations). This includes cross-site `fetch()` GETs, `\u003cscript\u003e`\nincludes, `\u003cimg\u003e` loads, and iframe embeds. For CSRF-like protection that only\nblocks cross-site state-changing requests, use the `LAX` preset:\n\n```python\nFETCH_METADATA_PRESET = 'LAX'\n```\n\nTo enable system checks, add `'fetch_metadata'` to `INSTALLED_APPS`.\n\n## Presets\n\nFour named presets cover common configurations:\n\n| Preset | Blocks cross-site GETs | Blocks navigations | Fail Open | Use Case |\n|--------|----------------------|-------------------|-----------|----------|\n| **DEFAULT** | Yes | Link clicks allowed | Yes | Full resource isolation |\n| **LAX** | No | No | Yes | CSRF-like protection |\n| **API** | Yes | Yes | Yes | API endpoints |\n| **STRICT** | Yes | Yes | No | Admin panels, internal tools |\n\nAny settings you specify explicitly will override the preset values.\n\nSee [Presets](docs/presets.md) for detailed scenarios.\n\n## Configuration\n\nAll settings are optional. Each preset works without any configuration.\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `FETCH_METADATA_PRESET` | `'DEFAULT'` | Named preset: `DEFAULT`, `LAX`, `API`, or `STRICT` |\n| `FETCH_METADATA_ALLOWED_SITES` | preset | List of allowed `Sec-Fetch-Site` values |\n| `FETCH_METADATA_ALLOW_NAVIGATIONS` | preset | Allow cross-site `navigate` + GET/HEAD |\n| `FETCH_METADATA_ALLOW_SAFE_METHODS` | preset | Allow all cross-site GET/HEAD requests |\n| `FETCH_METADATA_FAIL_OPEN` | preset | Pass requests with no `Sec-Fetch-Site` header |\n| `FETCH_METADATA_REPORT_ONLY` | `False` | Log violations without blocking |\n| `FETCH_METADATA_EXEMPT_PATHS` | `[]` | Path prefixes to skip (e.g. `['/.well-known/']`) |\n| `FETCH_METADATA_FAILURE_VIEW` | `None` | Dotted path to a custom 403 view |\n\nSee [Configuration](docs/configuration.md) for details.\n\n## Per-View Decorators\n\nExempt a view from all checks:\n\n```python\nfrom fetch_metadata.decorators import fetch_metadata_exempt\n\n@fetch_metadata_exempt\nclass WebhookView(View):\n    ...\n```\n\nOverride the policy for a specific view:\n\n```python\nfrom fetch_metadata.decorators import fetch_metadata_policy\n\n@fetch_metadata_policy(allowed_sites=['same-origin', 'same-site', 'none'])\nclass SubdomainAPIView(View):\n    ...\n```\n\nBoth decorators work on function-based views too:\n\n```python\n@fetch_metadata_exempt\ndef webhook_receiver(request):\n    ...\n```\n\n## Test Utilities\n\n`FetchMetadataTestMixin` provides assertion helpers for testing views against\nthe policy:\n\n```python\nfrom django.test import TestCase\nfrom fetch_metadata.test import FetchMetadataTestMixin\n\nclass MyViewTests(FetchMetadataTestMixin, TestCase):\n    def test_cross_site_blocked(self):\n        self.assert_blocks('/api/data/')\n\n    def test_same_origin_allowed(self):\n        self.assert_allows('/api/data/')\n```\n\n`assert_blocks` sends a cross-site POST by default. `assert_allows` sends a\nsame-origin POST. Both accept `method`, `site`, and `mode` keyword arguments.\n\n## How It Works\n\nThe middleware runs on every request via Django's `process_view` hook:\n\n1. **OPTIONS** requests always pass (CORS preflight carries no credentials)\n2. Exempt views and paths skip all checks\n3. The active policy is resolved (per-view decorator, or global preset + overrides)\n4. Missing `Sec-Fetch-Site` header: pass if `FAIL_OPEN`, block if not\n5. `Sec-Fetch-Site` value is in `ALLOWED_SITES` (e.g. `same-origin`): pass\n6. Request is GET/HEAD and `ALLOW_SAFE_METHODS` is enabled: pass\n7. Request is a cross-site link click (GET/HEAD with `Sec-Fetch-Mode: navigate`) and `ALLOW_NAVIGATIONS` is enabled: pass\n8. Everything else: log at WARNING and block (or pass in report-only mode)\n\nCross-site POSTs are blocked under all presets. Cross-site GETs depend on the\npreset: DEFAULT blocks them (except link clicks), LAX allows them all.\n\n## Common Patterns\n\n**Subdomain setup** (allow requests from other subdomains):\n\n```python\nFETCH_METADATA_ALLOWED_SITES = ['same-origin', 'same-site', 'none']\n```\n\n**Webhook endpoint exemption:**\n\n```python\nFETCH_METADATA_EXEMPT_PATHS = ['/webhooks/']\n```\n\n**Report-only rollout** (log violations without blocking, then review logs):\n\n```python\nFETCH_METADATA_REPORT_ONLY = True\n```\n\nViolations are logged to the `fetch_metadata` logger at WARNING level.\n\n## Further Reading\n\n- [Configuration](docs/configuration.md) - all settings, path exemptions, custom failure views\n- [Presets](docs/presets.md) - preset details with request flow traces\n- [W3C Fetch Metadata spec](https://www.w3.org/TR/fetch-metadata/) - the underlying browser mechanism\n- [web.dev: Protect your resources](https://web.dev/articles/fetch-metadata) - Google's implementation guide\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmptrluke%2Fdjango-fetch-metadata","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdmptrluke%2Fdjango-fetch-metadata","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdmptrluke%2Fdjango-fetch-metadata/lists"}