Resource isolation policy for Django using Fetch Metadata request headers.
Project description
django-fetch-metadata
Resource isolation policy for Django using Fetch Metadata request headers.
Browsers send Sec-Fetch-Site and Sec-Fetch-Mode headers on every request,
indicating where the request came from and how it was initiated. This middleware
uses those headers to block cross-site attacks while allowing legitimate
same-origin requests and direct navigations.
This is a defense-in-depth layer that works alongside Django's CSRF middleware, not a replacement for it. Non-browser clients that don't send Fetch Metadata headers (curl, API consumers, webhooks) pass through by default.
Installation
pip install django-fetch-metadata
Add the middleware to your MIDDLEWARE setting, before CsrfViewMiddleware:
MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'fetch_metadata.middleware.FetchMetadataMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
The DEFAULT preset is opinionated: it blocks all cross-site requests except
link clicks (navigations). This includes cross-site fetch() GETs, <script>
includes, <img> loads, and iframe embeds. For CSRF-like protection that only
blocks cross-site state-changing requests, use the LAX preset:
FETCH_METADATA_PRESET = 'LAX'
To enable system checks, add 'fetch_metadata' to INSTALLED_APPS.
Presets
Four named presets cover common configurations:
| Preset | Blocks cross-site GETs | Blocks navigations | Fail Open | Use Case |
|---|---|---|---|---|
| DEFAULT | Yes | Link clicks allowed | Yes | Full resource isolation |
| LAX | No | No | Yes | CSRF-like protection |
| API | Yes | Yes | Yes | API endpoints |
| STRICT | Yes | Yes | No | Admin panels, internal tools |
Any settings you specify explicitly will override the preset values.
See Presets for detailed scenarios.
Configuration
All settings are optional. Each preset works without any configuration.
| Setting | Default | Description |
|---|---|---|
FETCH_METADATA_PRESET |
'DEFAULT' |
Named preset: DEFAULT, LAX, API, or STRICT |
FETCH_METADATA_ALLOWED_SITES |
preset | List of allowed Sec-Fetch-Site values |
FETCH_METADATA_ALLOW_NAVIGATIONS |
preset | Allow cross-site navigate + GET/HEAD |
FETCH_METADATA_ALLOW_SAFE_METHODS |
preset | Allow all cross-site GET/HEAD requests |
FETCH_METADATA_FAIL_OPEN |
preset | Pass requests with no Sec-Fetch-Site header |
FETCH_METADATA_REPORT_ONLY |
False |
Log violations without blocking |
FETCH_METADATA_EXEMPT_PATHS |
[] |
Path prefixes to skip (e.g. ['/.well-known/']) |
FETCH_METADATA_FAILURE_VIEW |
None |
Dotted path to a custom 403 view |
See Configuration for details.
Per-View Decorators
Exempt a view from all checks:
from fetch_metadata.decorators import fetch_metadata_exempt
@fetch_metadata_exempt
class WebhookView(View):
...
Override the policy for a specific view:
from fetch_metadata.decorators import fetch_metadata_policy
@fetch_metadata_policy(allowed_sites=['same-origin', 'same-site', 'none'])
class SubdomainAPIView(View):
...
Both decorators work on function-based views too:
@fetch_metadata_exempt
def webhook_receiver(request):
...
Test Utilities
FetchMetadataTestMixin provides assertion helpers for testing views against
the policy:
from django.test import TestCase
from fetch_metadata.test import FetchMetadataTestMixin
class MyViewTests(FetchMetadataTestMixin, TestCase):
def test_cross_site_blocked(self):
self.assert_blocks('/api/data/')
def test_same_origin_allowed(self):
self.assert_allows('/api/data/')
assert_blocks sends a cross-site POST by default. assert_allows sends a
same-origin POST. Both accept method, site, and mode keyword arguments.
How It Works
The middleware runs on every request via Django's process_view hook:
- OPTIONS requests always pass (CORS preflight carries no credentials)
- Exempt views and paths skip all checks
- The active policy is resolved (per-view decorator, or global preset + overrides)
- Missing
Sec-Fetch-Siteheader: pass ifFAIL_OPEN, block if not Sec-Fetch-Sitevalue is inALLOWED_SITES(e.g.same-origin): pass- Request is GET/HEAD and
ALLOW_SAFE_METHODSis enabled: pass - Request is a cross-site link click (GET/HEAD with
Sec-Fetch-Mode: navigate) andALLOW_NAVIGATIONSis enabled: pass - Everything else: log at WARNING and block (or pass in report-only mode)
Cross-site POSTs are blocked under all presets. Cross-site GETs depend on the preset: DEFAULT blocks them (except link clicks), LAX allows them all.
Common Patterns
Subdomain setup (allow requests from other subdomains):
FETCH_METADATA_ALLOWED_SITES = ['same-origin', 'same-site', 'none']
Webhook endpoint exemption:
FETCH_METADATA_EXEMPT_PATHS = ['/webhooks/']
Report-only rollout (log violations without blocking, then review logs):
FETCH_METADATA_REPORT_ONLY = True
Violations are logged to the fetch_metadata logger at WARNING level.
Further Reading
- Configuration - all settings, path exemptions, custom failure views
- Presets - preset details with request flow traces
- W3C Fetch Metadata spec - the underlying browser mechanism
- web.dev: Protect your resources - Google's implementation guide
License
MIT. See LICENSE.
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 django_fetch_metadata-0.2.1.tar.gz.
File metadata
- Download URL: django_fetch_metadata-0.2.1.tar.gz
- Upload date:
- Size: 40.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb33628dcdfed0289075add0b4cd19f4217925a20f21f452b6bbdec6b38ee8ba
|
|
| MD5 |
5567eda28f355d096cb0fabfac5bd3a2
|
|
| BLAKE2b-256 |
7f98145f7be41a3ef2e96ea02083a18e68756a9d86a45a3b84a0be06d49b2446
|
Provenance
The following attestation bundles were made for django_fetch_metadata-0.2.1.tar.gz:
Publisher:
release.yml on dmptrluke/django-fetch-metadata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_fetch_metadata-0.2.1.tar.gz -
Subject digest:
bb33628dcdfed0289075add0b4cd19f4217925a20f21f452b6bbdec6b38ee8ba - Sigstore transparency entry: 1201645087
- Sigstore integration time:
-
Permalink:
dmptrluke/django-fetch-metadata@79248914edaac023efd1344c3e51c5627716a335 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/dmptrluke
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@79248914edaac023efd1344c3e51c5627716a335 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_fetch_metadata-0.2.1-py3-none-any.whl.
File metadata
- Download URL: django_fetch_metadata-0.2.1-py3-none-any.whl
- Upload date:
- Size: 14.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26bd02f8c37c3a4db186634f8a5ff21bf35cb73e7900781ea9bf1c3a13530cbb
|
|
| MD5 |
940dd850c7299b1c79fabc117b1128c1
|
|
| BLAKE2b-256 |
20436455006839b284eb4dab4a739f2ad4f947aa3e6c5f02dd10cd045af401a7
|
Provenance
The following attestation bundles were made for django_fetch_metadata-0.2.1-py3-none-any.whl:
Publisher:
release.yml on dmptrluke/django-fetch-metadata
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_fetch_metadata-0.2.1-py3-none-any.whl -
Subject digest:
26bd02f8c37c3a4db186634f8a5ff21bf35cb73e7900781ea9bf1c3a13530cbb - Sigstore transparency entry: 1201645093
- Sigstore integration time:
-
Permalink:
dmptrluke/django-fetch-metadata@79248914edaac023efd1344c3e51c5627716a335 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/dmptrluke
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@79248914edaac023efd1344c3e51c5627716a335 -
Trigger Event:
push
-
Statement type: