Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to execute unwanted actions on a web application. In this demo, the vulnerable version lacks CSRF tokens, allowing attackers to trick users into submitting malicious requests that appear to come from the legitimate user.
from flask_wtf.csrf import CSRFProtect, generate_csrfcsrf = CSRFProtect(app)# In templates# {{ csrf_token() }}# For AJAX requests@app.route('/get-csrf-token')def get_csrf_token(): return {'csrf_token': generate_csrf()}
AJAX Usage:
// Get token from meta tagconst token = document.querySelector('meta[name="csrf-token"]').content;// Include in AJAX requestfetch('/api/action', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': token }, body: JSON.stringify(data)});
2. SameSite Cookie Attribute
Configuration:
# Strict: Never sent on cross-site requestsapp.config['SESSION_COOKIE_SAMESITE'] = 'Strict'# Lax: Sent on top-level navigation (recommended)app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'# None: Always sent (requires Secure=True)app.config['SESSION_COOKIE_SAMESITE'] = 'None'app.config['SESSION_COOKIE_SECURE'] = True
Comparison:
Scenario
Strict
Lax
None
Same-site POST
Sent
Sent
Sent
Cross-site POST
NOT sent
NOT sent
Sent
Cross-site GET (link)
NOT sent
Sent
Sent
Cross-site GET (iframe)
NOT sent
NOT sent
Sent
Recommendation: Use Lax + CSRF tokens for defense in depth
3. Custom Request Headers
For AJAX APIs, require custom headers:
@app.before_requestdef check_custom_header(): if request.method in ['POST', 'PUT', 'DELETE']: if request.headers.get('X-Requested-With') != 'XMLHttpRequest': abort(403, "Missing required header")
Why this works: JavaScript can add custom headers, but CSRF attacks using forms cannot
@app.before_requestdef validate_origin(): if request.method in ['POST', 'PUT', 'DELETE']: origin = request.headers.get('Origin') referer = request.headers.get('Referer') allowed_origins = [ 'https://auth-secure.onrender.com', 'https://app.example.com' ] # Check Origin header (preferred) if origin: if origin not in allowed_origins: abort(403, "Invalid origin") # Fall back to Referer elif referer: if not any(referer.startswith(origin) for origin in allowed_origins): abort(403, "Invalid referer") else: # No Origin or Referer - suspicious abort(403, "Missing origin/referer")
Limitations: Headers can be stripped by proxies/browsers
5. Double-Submit Cookie Pattern
Alternative to server-side token storage:
from flask import make_responseimport secrets@app.route('/login', methods=['GET'])def login_form(): # Generate random token csrf_token = secrets.token_hex(32) response = make_response(render_template('login.html', csrf_token=csrf_token)) # Set as cookie AND include in form response.set_cookie('csrf_token', csrf_token, httponly=False, # JavaScript needs to read it secure=True, samesite='Strict') return response@app.route('/login', methods=['POST'])def login_submit(): # Compare cookie value with form value cookie_token = request.cookies.get('csrf_token') form_token = request.form.get('csrf_token') if not cookie_token or cookie_token != form_token: abort(403, "CSRF validation failed") # Process login...
How it works: Attacker can’t read victim’s cookie (same-origin policy) to include matching token in form
6. Re-authentication for Sensitive Actions
Require password for critical operations:
@app.route('/delete-account', methods=['POST'])def delete_account(): # Require password confirmation password = request.form.get('current_password') if not verify_password(session['user_id'], password): flash('Contraseña incorrecta', 'danger') return redirect('/account') # Proceed with deletion delete_user(session['user_id']) return redirect('/')
Benefits: Even if CSRF bypassed, attacker needs victim’s password