Start learning free

LESSON 11 OF 20 · SECURITY: TOKENS AND SESSIONS

CSRF Tokens

Practice

With a session cookie, the browser proves who you are on every request to the shop, even a request that another website starts. A page on another site can hold a hidden form that posts to the shop: when a logged-in visitor opens that page, the browser sends the form along with the shop's cookie. This is cross-site request forgery (CSRF).

The hole: the starting code changes the address for any POST that carries Ana's cookie. A prize page on another site changes her delivery address to the attacker's, and her next order ships to them.

The fix: a CSRF token. At login the shop stores a random token in the session. The shop's own page reads it from GET /account (another site can make the browser send requests, but can't read the shop's answers) and sends it back in an X-CSRF-Token header, which a plain form can't add. Before every POST, PUT and DELETE from a logged-in visitor, the shop compares the header with the session's token using hmac.compare_digest, and answers 403 when they differ.

A before_request function runs before every route; if it returns an answer, the route doesn't run. This one refuses changes while the shop is closed for maintenance:

@app.before_request
def read_only_mode():
    if MAINTENANCE and request.method != "GET":
        return jsonify({"error": "Back soon"}), 503

The check logs in with a form, then plays both the attacker and Ana's own page. <csrf> stands for the random token:

POST /login  form email=ana@example.com, password=ana-pass-123  →  200  {"message": "Logged in"}
GET /account  →  200  {"address": "Str. Florilor 3, Cluj", "csrf_token": "<csrf>", "email": "ana@example.com"}
POST /account/address  form address=Evil Street 1, Faraway, no header  →  403  {"error": "Missing or wrong CSRF token"}
POST /account/address  the same form, X-CSRF-Token: 0123456789abcdef0123456789abcdef  →  403  {"error": "Missing or wrong CSRF token"}
POST /account/address  form address=Str. Lunga 5, Cluj, X-CSRF-Token: <csrf>  →  200  {"address": "Str. Lunga 5, Cluj"}
GET /account  →  200  {"address": "Str. Lunga 5, Cluj", "csrf_token": "<csrf>", "email": "ana@example.com"}
POST /logout  no header  →  403  {"error": "Missing or wrong CSRF token"}
POST /logout  X-CSRF-Token: <csrf>  →  200  {"message": "Logged out"}
POST /login  the same form, no header  →  200  {"message": "Logged in"}

Your task: at login, store a random "csrf_token" in the session with secrets.token_hex(16); add it to the GET /account answer; and write a before_request function, check_csrf_token, that answers 403 with "Missing or wrong CSRF token" when a logged-in visitor's POST, PUT or DELETE doesn't carry the right X-CSRF-Token.


Assignment
Follow the Coding Tutorial to add a CSRF token and check it before every request that changes something.


Hint
Right after you add the decorator alone, it sits on login, so most requests answer 401 until check_csrf_token is written below it: that's expected. Read the header with request.headers.get("X-CSRF-Token", ""), so a missing header becomes an empty string that never matches.


STEP 1 OF 8
    Ctrl + Enter to check