Typorithm · medium

Flask: browser-session CSRF protection with Flask-WTF

pythonflaskindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Flask: browser-session CSRF protection with Flask-WTF

0%

Concept and contract

Verify a token belongs to the same session before accepting a browser write.

Browser cookies are attached to requests under browser rules, so an authenticated write needs a deliberate CSRF policy. A session-bound token demonstrates that the request obtained the expected form or token flow.

Flask does not enable CSRF protection for every application automatically. This lesson uses Flask-WTF as an explicit extension and keeps protection enabled in the test fixture.

Mechanism and worked example

The owned client first retrieves generate_csrf output, retaining the corresponding session cookie.

The owned client first retrieves generate_csrf output, retaining the corresponding session cookie. It then posts the token through the extension’s recognized X-CSRFToken header.

Correct, missing and invalid tokens produce distinct observed outcomes. A token copied from an unrelated session is not equivalent; the token and session participate in the same check rather than acting as an independent public password.

Complete worked example

python

from flask import Flask, request, session
from flask_wtf.csrf import CSRFProtect, generate_csrf

def csrf_request(mode):
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-csrf-key')
    CSRFProtect(app)
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/save')
    def save():
        return {'saved': True}, 201
    client = app.test_client()
    token = client.get('/token').json['token']
    headers = {'X-CSRFToken': token} if mode == 'correct' else {}
    if mode == 'wrong':
        headers['X-CSRFToken'] = 'invalid-owned-fixture'
    return client.post('/save', headers=headers).status_code
Reasoning
The owned client first retrieves generate_csrf output, retaining the corresponding session cookie. It then posts the token through the extension’s recognized X-CSRFToken header.

Counterexamples and common mistakes

The counterexample disables WTF_CSRF_ENABLED, making missing and invalid requests succeed.

The counterexample disables WTF_CSRF_ENABLED, making missing and invalid requests succeed. That is a test-environment trap: TESTING alone should not be treated as evidence that protection is exercised.

CSRF defenses also do not replace authentication, ownership checks or input validation. A request can carry a valid token and still lack permission to modify the requested resource.

Counterexample: a runnable framework mistake

python

from flask import Flask, request, session
from flask_wtf.csrf import CSRFProtect, generate_csrf

def csrf_request(mode):
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-csrf-key')
    app.config['WTF_CSRF_ENABLED'] = False
    CSRFProtect(app)
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/save')
    def save():
        return {'saved': True}, 201
    client = app.test_client()
    token = client.get('/token').json['token']
    headers = {'X-CSRFToken': token} if mode == 'correct' else {}
    if mode == 'wrong':
        headers['X-CSRFToken'] = 'invalid-owned-fixture'
    return client.post('/save', headers=headers).status_code
Reasoning
The counterexample disables WTF_CSRF_ENABLED, making missing and invalid requests succeed. That is a test-environment trap: TESTING alone should not be treated as evidence that protection is exercised.

Independent practice: one bounded framework change

Add a foreign-session mode: obtain a token using a separate test client and send it with the original client’s cookie.

Add a foreign-session mode: obtain a token using a separate test client and send it with the original client’s cookie. Expect rejection.

Preserve the valid same-session request and existing missing-token behavior. This is a small test-flow extension rather than a new security algorithm; use the extension’s public API and do not reproduce its signing internals.

Step 1: Add a foreign-session token case using a second client. Step 2: Keep the original client and its cookie.

Practice starter: solve before reading the solution

python

from flask import Flask, request, session
from flask_wtf.csrf import CSRFProtect, generate_csrf

def csrf_request(mode):
    # Remove this placeholder, then adapt the retained baseline:
    # Add a foreign-session token case using a second client.
    raise NotImplementedError
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-csrf-key')
    CSRFProtect(app)
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/save')
    def save():
        return {'saved': True}, 201
    client = app.test_client()
    token = client.get('/token').json['token']
    headers = {'X-CSRFToken': token} if mode == 'correct' else {}
    if mode == 'wrong':
        headers['X-CSRFToken'] = 'invalid-owned-fixture'
    return client.post('/save', headers=headers).status_code
Reasoning
Add a foreign-session token case using a second client. Keep the original client and its cookie.
  • Hint 1: Obtain the foreign token from another client.
  • Hint 2: Post it through the original session.

Solution, tests and trade-offs

The baseline asserts 201 only for the valid token and 400 for missing or malformed tokens.

The baseline asserts 201 only for the valid token and 400 for missing or malformed tokens. Practice adds a genuine second session, so a hard-coded invalid token cannot stand in for the cross-session check.

All clients and keys belong to the local fixture. The suite does not contact an identity provider or change any real browser session.

Practice solution and acceptance checks

python

from flask import Flask, request, session
from flask_wtf.csrf import CSRFProtect, generate_csrf

def csrf_request(mode):
    app = Flask(__name__)
    app.config.update(TESTING=True, SECRET_KEY='owned-csrf-key')
    CSRFProtect(app)
    @app.get('/token')
    def token():
        return {'token': generate_csrf()}
    @app.post('/save')
    def save():
        return {'saved': True}, 201
    client = app.test_client()
    token = client.get('/token').json['token']
    headers = {'X-CSRFToken': token} if mode == 'correct' else {}
    if mode == 'foreign':
        foreign = app.test_client()
        headers['X-CSRFToken'] = foreign.get('/token').json['token']
    if mode == 'wrong':
        headers['X-CSRFToken'] = 'invalid-owned-fixture'
    return client.post('/save', headers=headers).status_code
Reasoning
The baseline asserts 201 only for the valid token and 400 for missing or malformed tokens. Practice adds a genuine second session, so a hard-coded invalid token cannot stand in for the cross-session check.

Interview reasoning and next step

For a production application choose a coherent cookie, HTTPS and CSRF policy and document which endpoints use browser credentials..

For a production application choose a coherent cookie, HTTPS and CSRF policy and document which endpoints use browser credentials.

Token-based non-cookie API authentication has different exposure patterns, so do not blindly exempt all JSON endpoints. Keep extension versions maintained, handle errors without exposing secrets and test the same protections through the deployed request boundary as well as the local client.

Optional stretch — after the core task works: Add an authorized form with validation and persistence.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track flask, then edit the retained exercise baseline and check it with --practice fl-csrf. The lab uses owned in-memory databases, local framework clients and tiny CPU tensors. It never reads the working ApplyDjinn database, contacts production feeds or downloads models.

Verify a token belongs to the same session before accepting a browser write.