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.
- 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
pip install flask-sec-fetch-csrffrom 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| 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 |
By default, POST, PUT, PATCH, and DELETE requests are protected:
# Only protect POST requests
app.config['SEC_FETCH_CSRF_METHODS'] = ['POST']If you trust all subdomains of your site:
# Allow requests from *.example.com to example.com
app.config['SEC_FETCH_CSRF_ALLOW_SAME_SITE'] = TrueWarning: Only enable this if you trust all subdomains. A compromised subdomain could perform CSRF attacks.
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',
]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'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'}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'}, 403The 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.
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.
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>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.
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
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.
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
| 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 |
-
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
If you're currently using Flask-WTF's CSRFProtect, migration is straightforward:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- form fields -->
</form>from flask_sec_fetch_csrf import SecFetchCSRF
csrf = SecFetchCSRF(app)<form method="POST">
<!-- No token needed! -->
<!-- form fields -->
</form>| 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) |
- 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
See the examples directory for a demo application that shows CSRF protection in action, including how to simulate cross-site attacks.
- Filippo Valsorda: "CSRF" — The algorithm this extension implements
- MDN: Cross-Site Request Forgery
- MDN: Sec-Fetch-Site
- OWASP: CSRF Prevention Cheat Sheet
BSD-3-Clause License. See LICENSE for details.
