CSRF protection for Flask using Sec-Fetch-Site header
Project description
CSRF protection for Flask using the Sec-Fetch-Site header
This extension protects your Flask application from Cross-Site Request Forgery (CSRF) attacks by validating the Sec-Fetch-Site header sent by modern browsers. Unlike token-based CSRF protection, this approach requires no form modifications, no session storage, and no JavaScript integration.
Table of Contents
- Installation
- Quick Start
- Configuration
- Exempting Routes
- Error Handling
- Browser Support
- API Clients and Non-Browser Requests
- Motivation
- What is CSRF?
- How This Extension Protects You
- Comparison with Token-Based CSRF
- Security Considerations
- Migrating from Flask-WTF
- Examples
- References
- License
Installation
pip install flask-sec-fetch-csrf
Quick Start
from flask import Flask
from flask_sec_fetch_csrf import SecFetchCSRF
app = Flask(__name__)
csrf = SecFetchCSRF(app)
@app.route('/transfer', methods=['POST'])
def transfer():
# Protected automatically
return 'Transfer complete'
Or with the application factory pattern:
from flask_sec_fetch_csrf import SecFetchCSRF
csrf = SecFetchCSRF()
def create_app():
app = Flask(__name__)
csrf.init_app(app)
return app
Configuration
| Option | Default | Description |
|---|---|---|
SEC_FETCH_CSRF_METHODS |
["POST", "PUT", "PATCH", "DELETE"] |
HTTP methods to protect |
SEC_FETCH_CSRF_ALLOW_SAME_SITE |
False |
Allow requests from same site (subdomains) |
SEC_FETCH_CSRF_TRUSTED_ORIGINS |
[] |
Origins allowed for cross-site requests |
Protecting Specific Methods
By default, POST, PUT, PATCH, and DELETE requests are protected:
# Only protect POST requests
app.config['SEC_FETCH_CSRF_METHODS'] = ['POST']
Allowing Same-Site Requests
If you trust all subdomains of your site:
# Allow requests from *.example.com to example.com
app.config['SEC_FETCH_CSRF_ALLOW_SAME_SITE'] = True
Warning: Only enable this if you trust all subdomains. A compromised subdomain could perform CSRF attacks.
Trusted Origins
For legitimate cross-origin requests (e.g., from a separate frontend):
app.config['SEC_FETCH_CSRF_TRUSTED_ORIGINS'] = [
'https://app.example.com',
'https://admin.example.com',
]
Exempting Routes
Exempt a View
Use the @csrf.exempt decorator for endpoints that need to accept cross-origin requests (e.g., webhooks):
@csrf.exempt
@app.route('/webhook', methods=['POST'])
def webhook():
# Accepts requests from anywhere
return 'OK'
Exempt a Blueprint
from flask import Blueprint
api = Blueprint('api', __name__)
csrf.exempt(api)
@api.route('/data', methods=['POST'])
def api_data():
# All routes in this blueprint are exempt
return {'status': 'ok'}
Error Handling
CSRF failures raise CSRFError (a 403 Forbidden response). Customize the response:
from flask_sec_fetch_csrf import CSRFError
@app.errorhandler(CSRFError)
def handle_csrf_error(error):
return {'error': 'CSRF validation failed'}, 403
Browser Support
The Sec-Fetch-Site header is supported in all modern browsers:
| Browser | Version | Release Date |
|---|---|---|
| Chrome | 76+ | July 2019 |
| Edge | 79+ | January 2020 |
| Firefox | 90+ | July 2021 |
| Safari | 16.4+ | March 2023 |
For older browsers without Sec-Fetch-Site support, the extension falls back to Origin header validation.
API Clients and Non-Browser Requests
Requests without browser headers (no Sec-Fetch-Site and no Origin) are allowed. This permits:
- API clients (requests, httpx, curl)
- Server-to-server communication
- Mobile apps using native HTTP clients
If a request has an Origin header but no Sec-Fetch-Site, the extension validates that the Origin matches the Host header.
Motivation
The HTMX Problem
If you've used HTMX with Flask-WTF's CSRF protection, you know the pain:
<!-- Every HTMX element needs the token -->
<button hx-post="/api/action"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'>
Click me
</button>
Or you set up global headers:
<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
document.body.addEventListener('htmx:configRequest', (event) => {
event.detail.headers['X-CSRFToken'] = document.querySelector('meta[name="csrf-token"]').content;
});
</script>
This is tedious, error-prone, and breaks when tokens expire. With Sec-Fetch-Site, HTMX just works:
<!-- No token needed. The browser handles it. -->
<button hx-post="/api/action">Click me</button>
Token Fatigue
Traditional CSRF tokens create ongoing friction:
- Cached pages serve stale tokens
- Expired sessions invalidate tokens mid-form
- AJAX requests need manual token injection
- Multi-tab usage can cause token mismatches
- API clients need special handling to skip tokens
The Sec-Fetch-Site header eliminates all of this. The browser sends it automatically, it never expires, and it works consistently across all request types.
Inspiration
This extension was inspired by:
- Rails PR #56350 — Rails 8.2 is adopting
Sec-Fetch-Siteas its primary CSRF defense, moving away from tokens - Flask-WTF — The established Flask CSRF solution, whose API patterns influenced this extension
- Filippo Valsorda's "CSRF" — The algorithm and rationale behind header-based CSRF protection
What is CSRF?
Cross-Site Request Forgery (CSRF) is an attack that tricks users into performing unwanted actions on a website where they're authenticated.
How it works:
- You log into your bank at
bank.example.com - Your browser stores a session cookie
- You visit a malicious site that contains:
<form action="https://bank.example.com/transfer" method="POST"> <input type="hidden" name="to" value="attacker"> <input type="hidden" name="amount" value="10000"> </form> <script>document.forms[0].submit();</script>
- Your browser sends the request with your session cookie
- The bank processes the transfer because it looks like a legitimate request
The key insight is that browsers automatically include cookies with requests, even when those requests originate from other sites.
How This Extension Protects You
Modern browsers send the Sec-Fetch-Site header with every request, indicating where the request originated:
| Value | Meaning | Action |
|---|---|---|
same-origin |
Request from same origin (scheme + host + port) | Allow |
none |
User typed URL or used bookmark | Allow |
same-site |
Request from same site (e.g., subdomain) | Deny by default |
cross-site |
Request from different site | Deny |
This extension implements the algorithm recommended by Filippo Valsorda:
- Allow safe methods — GET, HEAD, OPTIONS don't modify state
- Check trusted origins — Explicitly allowed cross-origin sources
- Validate Sec-Fetch-Site — Allow
same-originornone, reject others - Handle missing header — Allow if no
Originheader either (non-browser client) - Fallback to Origin — For older browsers, compare
OriginagainstHost
Comparison with Token-Based CSRF
| Aspect | Token-Based | Sec-Fetch-Site |
|---|---|---|
| Form modifications | Required | None |
| Session storage | Required | None |
| JavaScript integration | Often needed | None |
| Setup complexity | Moderate | Minimal |
| Browser support | Universal | Modern (with fallback) |
| Protection strength | Strong | Strong |
Security Considerations
-
HTTPS Required —
Sec-Fetch-Siteis only sent on secure connections (HTTPS, localhost) -
Defense in Depth — Consider combining with
SameSitecookies:app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
-
XSS Defeats CSRF Protection — If your site has XSS vulnerabilities, attackers can bypass any CSRF protection
-
Subdomain Trust — Keep
SEC_FETCH_CSRF_ALLOW_SAME_SITEdisabled unless you trust all subdomains
Migrating from Flask-WTF
If you're currently using Flask-WTF's CSRFProtect, migration is straightforward:
Before (Flask-WTF)
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- form fields -->
</form>
After (flask-sec-fetch-csrf)
from flask_sec_fetch_csrf import SecFetchCSRF
csrf = SecFetchCSRF(app)
<form method="POST">
<!-- No token needed! -->
<!-- form fields -->
</form>
Key Differences
| Flask-WTF | flask-sec-fetch-csrf |
|---|---|
Requires {{ csrf_token() }} in forms |
No form changes needed |
Uses WTF_CSRF_* config keys |
Uses SEC_FETCH_CSRF_* config keys |
| Returns 400 Bad Request on failure | Returns 403 Forbidden on failure |
@csrf.exempt decorator |
@csrf.exempt decorator (same API) |
Migration Checklist
- Replace
from flask_wtf.csrf import CSRFProtectwithfrom flask_sec_fetch_csrf import SecFetchCSRF - Rename config keys from
WTF_CSRF_*toSEC_FETCH_CSRF_* - Update error handlers to expect 403 instead of 400
- Remove
{{ csrf_token() }}from templates - Remove any JavaScript that handles CSRF tokens in AJAX requests
Examples
See the examples directory for a demo application that shows CSRF protection in action, including how to simulate cross-site attacks.
References
- Filippo Valsorda: "CSRF" — The algorithm this extension implements
- MDN: Cross-Site Request Forgery
- MDN: Sec-Fetch-Site
- OWASP: CSRF Prevention Cheat Sheet
License
BSD-3-Clause License. See LICENSE for details.
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 flask_sec_fetch_csrf-0.2.0.tar.gz.
File metadata
- Download URL: flask_sec_fetch_csrf-0.2.0.tar.gz
- Upload date:
- Size: 8.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b3b454bc796a43904838fb47bad0ea835768fc0ae9f2724355870e30b9cc3ad
|
|
| MD5 |
1d116716b4f326af1e533284f2a60a2d
|
|
| BLAKE2b-256 |
845d9a7899def0cbf934cb7cf6713c61204463b8cc574315db772ce187d30666
|
File details
Details for the file flask_sec_fetch_csrf-0.2.0-py3-none-any.whl.
File metadata
- Download URL: flask_sec_fetch_csrf-0.2.0-py3-none-any.whl
- Upload date:
- Size: 9.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
167a65f2b9ef7662667353fd104293a94cf8d8bcd5fb70d2d042569c2b0d9fdf
|
|
| MD5 |
f768e813ad5f385f39e6208c46809897
|
|
| BLAKE2b-256 |
266236903a1a9f13371c90b2fa222060c9116059ec5e858a6c08d0efea693aa3
|