Typorithm · medium

CSRF checks must actually be enabled in tests

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

Step 1 of 4 · intro card

CSRF checks must actually be enabled in tests

0%

Concept and contract

Exercise the browser request boundary with a real cookie/token pair and explicit enforcement.

A browser can attach an authenticated session cookie to a request the user did not intend to initiate. CSRF protection addresses that request-origin boundary, which is different from checking login or object ownership.

Django’s default test client deliberately relaxes CSRF checks for convenience. This lesson turns enforcement on and obtains its token through the same client, making a successful state-changing request depend on the actual protection path.

Mechanism and worked example

The initial GET asks get_token for a token and lets the middleware set the corresponding cookie.

The initial GET asks get_token for a token and lets the middleware set the corresponding cookie. The later POST supplies that token in the expected header.

Missing or malformed tokens receive a forbidden response instead of the fictional save result. Random token bytes are not hardcoded in expected output; the test observes whether the legitimate pair works and invalid requests are rejected.

Complete worked example

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.test import Client
from django.urls import path
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def form_view(request):
    if request.method == "GET":
        return JsonResponse({"token": get_token(request)})
    return JsonResponse({"saved": True}, status=201)

urlpatterns = [path("form/", form_view)]

def csrf_request(mode):
    client = Client(enforce_csrf_checks=True)
    token = client.get("/form/").json()["token"]
    headers = {}
    if mode == "correct":
        headers["HTTP_X_CSRFTOKEN"] = token
    elif mode == "wrong":
        headers["HTTP_X_CSRFTOKEN"] = "invalid-token"
    return client.post("/form/", data={"title": "Owned fixture"}, **headers).status_code
Reasoning
The initial GET asks get_token for a token and lets the middleware set the corresponding cookie. The later POST supplies that token in the expected header.

Counterexamples and common mistakes

The counterexample uses the ordinary test client without explicit enforcement.

The counterexample uses the ordinary test client without explicit enforcement. A missing-token POST then succeeds and a test can falsely claim CSRF coverage.

Do not disable middleware merely to make a failing test green. Also do not treat possession of a token as ownership of every object or as permission to accept every origin. Browser transport, trusted origins, session policy and object authorization remain separate parts of the application contract.

Counterexample: a runnable framework mistake

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.test import Client
from django.urls import path
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def form_view(request):
    if request.method == "GET":
        return JsonResponse({"token": get_token(request)})
    return JsonResponse({"saved": True}, status=201)

urlpatterns = [path("form/", form_view)]

def csrf_request(mode):
    client = Client()
    token = client.get("/form/").json()["token"]
    headers = {}
    if mode == "correct":
        headers["HTTP_X_CSRFTOKEN"] = token
    elif mode == "wrong":
        headers["HTTP_X_CSRFTOKEN"] = "invalid-token"
    return client.post("/form/", data={"title": "Owned fixture"}, **headers).status_code
Reasoning
The counterexample uses the ordinary test client without explicit enforcement. A missing-token POST then succeeds and a test can falsely claim CSRF coverage.

Independent practice: one bounded framework change

Allow the fixture helper to supply an optional Origin header without changing the middleware configuration.

Allow the fixture helper to supply an optional Origin header without changing the middleware configuration. Verify that the correct token still does not authorize an untrusted origin, while the matching test origin remains accepted.

This adds one controlled request input rather than broadening CSRF_TRUSTED_ORIGINS. Keep the client’s enforcement flag and the original missing-token case intact so the practice cannot pass by turning protection off.

Step 1: Add an optional Origin request header. Step 2: Keep the correct token insufficient for an untrusted origin.

Practice starter: solve before reading the solution

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.test import Client
from django.urls import path
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def form_view(request):
    if request.method == "GET":
        return JsonResponse({"token": get_token(request)})
    return JsonResponse({"saved": True}, status=201)

urlpatterns = [path("form/", form_view)]

def csrf_request(mode):
    # Remove this placeholder, then adapt the retained baseline:
    # Add an optional Origin request header.
    raise NotImplementedError
    client = Client(enforce_csrf_checks=True)
    token = client.get("/form/").json()["token"]
    headers = {}
    if mode == "correct":
        headers["HTTP_X_CSRFTOKEN"] = token
    elif mode == "wrong":
        headers["HTTP_X_CSRFTOKEN"] = "invalid-token"
    return client.post("/form/", data={"title": "Owned fixture"}, **headers).status_code
Reasoning
Add an optional Origin request header. Keep the correct token insufficient for an untrusted origin.
  • Hint 1: Pass the header to the request, not to a broader trusted-origin setting.
  • Hint 2: Retain the missing-token rejection case.

Solution, tests and trade-offs

The expected results cover the successful cookie/token exchange, a missing token and a malformed token.

The expected results cover the successful cookie/token exchange, a missing token and a malformed token. Practice adds a matching origin and an untrusted one.

These are actual middleware responses, not statuses invented by a helper. The fixture remains entirely local and contains no real session credential. Production HTTPS referer behavior and deployment proxy settings require their own correctly configured environment and tests.

Practice solution and acceptance checks

python

from bootstrap_lab import django_setup
django_setup(__name__)

from django.http import JsonResponse
from django.middleware.csrf import get_token
from django.test import Client
from django.urls import path
from django.views.decorators.http import require_http_methods

@require_http_methods(["GET", "POST"])
def form_view(request):
    if request.method == "GET":
        return JsonResponse({"token": get_token(request)})
    return JsonResponse({"saved": True}, status=201)

urlpatterns = [path("form/", form_view)]

def csrf_request(mode, origin=None):
    client = Client(enforce_csrf_checks=True)
    token = client.get("/form/").json()["token"]
    headers = {}
    if origin is not None:
        headers["HTTP_ORIGIN"] = origin
    if mode == "correct":
        headers["HTTP_X_CSRFTOKEN"] = token
    elif mode == "wrong":
        headers["HTTP_X_CSRFTOKEN"] = "invalid-token"
    return client.post("/form/", data={"title": "Owned fixture"}, **headers).status_code
Reasoning
The expected results cover the successful cookie/token exchange, a missing token and a malformed token. Practice adds a matching origin and an untrusted one.

Interview reasoning and next step

Explain why an authenticated session, a valid token and an owner-scoped object lookup answer different questions.

Explain why an authenticated session, a valid token and an owner-scoped object lookup answer different questions. Then identify which test-client option would accidentally bypass this lesson’s boundary.

A production application must also avoid caching personalized token-bearing responses as if they were public shared data. The cache lesson next makes representation identity explicit, so one request population cannot silently reuse another population’s saved result.

Optional stretch — after the core task works: Exercise the HTTPS referer branch in a separate owned deployment fixture.

Computer practice: https://applydjinn.com/typorithm/python-frameworks-lab-20261105.zip. Install the pinned dependencies in an isolated environment, run verify.py --track django, then edit the retained exercise baseline and check it with --practice dj-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.

Exercise the browser request boundary with a real cookie/token pair and explicit enforcement.