Goodbye Hidden Fields: Modern CSRF Protection Without Tokens
Source: Dev.to
If you have ever configured protection against Cross‑Site Request Forgery (CSRF) attacks, you likely remember the routine: generate unique tokens, embed them in hidden form fields, and ensure your scripts send the correct headers. In 2025 this process can be significantly simplified while making your code much cleaner.
Traditional CSRF Tokens
- State synchronization – Tokens required the server and client to keep shared state, complicating page caching.
- Markup bloat – Every form needed an extra hidden input.
- Debugging pain – Expired‑token errors consumed valuable development time.
These drawbacks made token‑based protection feel like an unavoidable technical debt.
Browser‑Based Protection with Fetch Metadata
Modern browsers now transmit a special header known as Fetch Metadata. The most critical element is the Sec-Fetch-Site header, which tells the server the true origin of a request.
| Request origin | Header value |
|---|---|
| Same‑origin form submission | Sec-Fetch-Site: same-origin |
| Cross‑site request (e.g., malicious site) | Sec-Fetch-Site: cross-site |
The browser guarantees that this header cannot be spoofed or modified via JavaScript, allowing the server to rely entirely on it.
Fallback for Older Browsers
If a browser does not send Fetch Metadata, you can fall back to checking the Origin or Referer headers. Requests with same-origin or none (e.g., direct URL entry) are allowed; others are rejected.
Implementation Example
Below are minimal examples for Node.js (Express) and Python (Flask) that reject cross‑site requests for state‑changing methods.
Express (Node.js)
// app.js
const express = require('express');
const app = express();
function csrfProtection(req, res, next) {
const method = req.method.toUpperCase();
const unsafeMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (!unsafeMethods.includes(method)) {
return next(); // Safe method, no check needed
}
const site = req.get('Sec-Fetch-Site');
if (site && site === 'cross-site') {
return res.status(403).send('Forbidden: CSRF protection');
}
// Fallback for older browsers
const origin = req.get('Origin') || req.get('Referer');
if (origin && !origin.includes(req.get('Host'))) {
return res.status(403).send('Forbidden: CSRF protection');
}
next();
}
app.use(express.json());
app.use(csrfProtection);
app.post('/api/data', (req, res) => {
// Handle state‑changing request
res.json({ status: 'success' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Flask (Python)
# app.py
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
def csrf_protect():
unsafe_methods = {'POST', 'PUT', 'PATCH', 'DELETE'}
if request.method not in unsafe_methods:
return # Safe method
site = request.headers.get('Sec-Fetch-Site')
if site == 'cross-site':
abort(403, description='Forbidden: CSRF protection')
# Fallback for older browsers
origin = request.headers.get('Origin') or request.headers.get('Referer')
if origin and request.host not in origin:
abort(403, description='Forbidden: CSRF protection')
app.before_request(csrf_protect)
@app.route('/api/data', methods=['POST'])
def handle_data():
# Process the request
return jsonify(status='success')
if __name__ == '__main__':
app.run(port=5000)
Benefits of Header‑Based CSRF Protection
- No token generation or storage – Eliminates session‑side state and reduces markup.
- Cache‑friendly – Forms can be cached without worrying about stale tokens.
- Simpler debugging – Errors are reduced to standard HTTP status codes.
- Performance gain – Fewer bytes transmitted and less server‑side processing.
Industry Recognition
The OWASP project has recognized Fetch Metadata as a viable alternative to classic anti‑CSRF tokens, moving it from experimental status to a recommended practice.
Conclusion
Browsers are becoming smarter and handling routine security tasks automatically. By shifting CSRF protection from hidden fields and tokens to reliable request metadata, you achieve the same level of security with a cleaner, lighter codebase. This marks a step toward a simpler, more maintainable web.