Skip to main content

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:

  1. OPTIONS requests always pass (CORS preflight carries no credentials)
  2. Exempt views and paths skip all checks
  3. The active policy is resolved (per-view decorator, or global preset + overrides)
  4. Missing Sec-Fetch-Site header: pass if FAIL_OPEN, block if not
  5. Sec-Fetch-Site value is in ALLOWED_SITES (e.g. same-origin): pass
  6. Request is GET/HEAD and ALLOW_SAFE_METHODS is enabled: pass
  7. Request is a cross-site link click (GET/HEAD with Sec-Fetch-Mode: navigate) and ALLOW_NAVIGATIONS is enabled: pass
  8. 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

License

MIT. See LICENSE.

Release files for django-fetch-metadata 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-fetch-metadata 0.2.1
File Size Uploaded
django_fetch_metadata-0.2.1.tar.gz 40.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-fetch-metadata 0.2.1
File Interpreter ABI Platform
django_fetch_metadata-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 55.5 kB

Release files / django_fetch_metadata-0.2.1.tar.gz

Download URL django_fetch_metadata-0.2.1.tar.gz
Size 40.9 kB
Tags Source
SHA-256 checksum
How to use checksums
bb33628dcdfed0289075add0b4cd19f4217925a20f21f452b6bbdec6b38ee8ba
BLAKE2b-256 checksum
How to use checksums
7f98145f7be41a3ef2e96ea02083a18e68756a9d86a45a3b84a0be06d49b2446
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 30, 2026.

Transparency log

Release files / django_fetch_metadata-0.2.1-py3-none-any.whl

Download URL django_fetch_metadata-0.2.1-py3-none-any.whl
Size 14.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
26bd02f8c37c3a4db186634f8a5ff21bf35cb73e7900781ea9bf1c3a13530cbb
BLAKE2b-256 checksum
How to use checksums
20436455006839b284eb4dab4a739f2ad4f947aa3e6c5f02dd10cd045af401a7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Mar 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page