Usage

Setup

Using the middleware is straightforward. You can wrap your ASGI app in it:

from piccolo_api.csrf.middleware import CSRFMiddleware

app = CSRFMiddleware(my_asgi_app, allowed_hosts=["foo.com"])

Or you can pass it to the middleware argument of your ASGI app. For example, with FastAPI:

from fastapi import FastAPI
from starlette.middleware import Middleware

app = FastAPI(middleware=[Middleware(CSRFMiddleware)])

Or Starlette:

from starlette import Starlette
from starlette.middleware import Middleware

app = Starlette(middleware=[Middleware(CSRFMiddleware)])

How it works

When the user makes a request, the middleware makes sure that a CSRF cookie is set on their device. This cookie contains a random token.

When a request is made with a non-safe method (e.g. POST), then the middleware checks for the cookie, and the token contained within the cookie either in a HTTP header or form field. If the token values don’t match, the request is rejected.

Note: You have to explicitly tell the middleware to look for the token in a form field:

app = CSRFMiddleware(my_asgi_app, allow_form_param=True)

It isn’t enabled by default, as adding it to the header is preferable (see below).


Accessing the CSRF token in HTML

As mentioned, you need to add the token contained within the CSRF cookie as a HTTP header or form field. There are two ways of accessing this value.

Template variable

Firstly, we can get the csrftoken value from the requests’ ASGI scope, and then insert it into the template context:

def my_endpoint(request: Request):
    csrftoken = request.scope.get('csrftoken')
    csrf_cookie_name = request.scope.get('csrf_cookie_name')

    template = ENVIRONMENT.get_template("example.html.jinja")
    content = template.render(
        csrftoken=csrftoken,
        csrf_cookie_name=csrf_cookie_name
    )

    return HTMLResponse(content)

And then within the HTML template:

<form method="POST">
    <label>Username</label>
    <input type="text" name="username" />
    <label>Password</label>
    <input type="password" name="password" />

    {% if csrftoken and csrf_cookie_name %}
        <input type="hidden" name="{{ csrf_cookie_name }}">{{ csrtoken }}</input>
    {% endif %}

    <button>Login</button>
</form>

Full example

In the the example below, we get the token from the cookie, and add it to the request header.

<form method="POST" onsubmit="login(event,this)">
    <label>Username</label>
    <input type="text" name="username" />
    <label>Password</label>
    <input type="password" name="password" />

    <button>Login</button>
</form>

<script src="https://cdnjs.cloudflare.com/ajax/libs/js-cookie/2.2.1/js.cookie.min.js"></script>

<script>
    function login(event, form) {
        const csrftoken = Cookies.get('csrftoken');
        event.preventDefault()
        fetch("/login/", {
            method: "POST",
            credentials: "same-origin",
            headers: {
                "X-CSRFToken": csrftoken,
                "Accept": "application/json",
                "Content-Type": "application/json"
            },
            body: JSON.stringify({ username: form.username.value, password: form.password.value })
        })
    }
</script>

Should I embed the token in the form, or add it as a HTTP header?

Setting the cookie in the header is preferable as:

  • It makes caching easier, as CSRF tokens aren’t embedded in HTML forms.

  • We no longer have to worry about BREACH attacks.

However, you can embed the CSRF token in the form if you want.

app = CSRFMiddleware(my_asgi_app, allow_form_param=True)

To guard against BREACH attacks, you can use rate limiting middleware on that endpoint, or just disable HTTP compression for your website.


Source

class piccolo_api.csrf.middleware.CSRFMiddleware(app: ASGIApp, allowed_hosts: Sequence[str] = [], cookie_name: str = DEFAULT_COOKIE_NAME, header_name: str = DEFAULT_HEADER_NAME, max_age: int = ONE_YEAR, allow_header_param: bool = True, allow_form_param: bool = False, **kwargs)[source]

For GET requests, set a random token as a cookie. For unsafe HTTP methods, require a HTTP header to match the cookie value, otherwise the request is rejected.

This uses the Double Submit Cookie style of CSRF prevention. For more information see the OWASP cheatsheet (double submit cookie and customer request headers).

By default, the CSRF token needs to be added to the request header. By setting allow_form_param to True, it will also work if added as a form parameter.

Parameters:
  • app – The ASGI app you want to wrap.

  • allowed_hosts – If using this middleware with HTTPS, you need to set this value, for example ['example.com'].

  • cookie_name – You can specify a custom name for the cookie. There should be no need to change it, unless in the rare situation where the name clashes with another cookie.

  • header_name – You can tell the middleware to look for the CSRF token in a different HTTP header.

  • max_age – The max age of the cookie, in seconds.

  • allow_header_param – Whether to look for the CSRF token in the HTTP headers.

  • allow_form_param – Whether to look for the CSRF token in a form field with the same name as the cookie. By default, it’s not enabled.