Understanding CSRF Attacks and How Flask Protects You

CSRF (Cross-Site Request Forgery) tricks a user's browser into making unintended requests to your app. It's silent, devastating, and completely preventable.

How CSRF Works

  1. User logs in to bank.com — their browser stores a session cookie
  2. User visits attacker's page evil.com
  3. evil.com has a hidden form that POSTs to bank.com/transfer
  4. The browser automatically sends the session cookie with the request
  5. The bank processes the transfer as if the user initiated it

The Attack in Code

<!-- evil.com/attack.html -->
<form action="https://bank.com/transfer" method="POST" id="f">
  <input name="to"     value="attacker-account">
  <input name="amount" value="10000">
</form>
<script>document.getElementById('f').submit();</script>

The victim doesn't click anything — the page auto-submits.

Protection: CSRF Tokens

A CSRF token is a random, unpredictable value tied to the user's session. Include it in every form; verify it on every POST.

Generating Tokens

import secrets
import hmac
import hashlib
from flask import session

def generate_csrf_token() -> str:
    if 'csrf_secret' not in session:
        session['csrf_secret'] = secrets.token_hex(32)
    token = secrets.token_hex(16)
    sig = hmac.new(
        session['csrf_secret'].encode(),
        token.encode(),
        hashlib.sha256
    ).hexdigest()
    return f"{token}.{sig}"

def validate_csrf(token: str) -> bool:
    if not token or '.' not in token:
        return False
    raw, sig = token.rsplit('.', 1)
    expected = hmac.new(
        session.get('csrf_secret', '').encode(),
        raw.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, expected)

Adding to Forms

<form method="POST">
  <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
  <!-- rest of form -->
</form>

Validating on POST

from flask import request, abort

def csrf_check():
    token = request.form.get('csrf_token') or request.headers.get('X-CSRFToken')
    if not validate_csrf(token):
        abort(403)

@app.route('/transfer', methods=['POST'])
def transfer():
    csrf_check()
    # safe to process
    ...

SameSite Cookies (Defence in Depth)

app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'  # or 'Strict'
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE']   = True    # HTTPS only

SameSite=Lax prevents the cookie from being sent on cross-site POST requests, which alone stops most CSRF attacks.

An alternative for stateless APIs: set a CSRF cookie client-side, read it in JavaScript, and send it as a header. The server checks that the header matches the cookie.

Security is layered — use CSRF tokens and SameSite cookies.