Security

CSRF protection

CSRF (Cross-Site Request Forgery) is an attack in which a malicious website tricks the users of your web app to perform form submissions without their explicit consent.

To protect against CSRF attacks, Expanse gives you two options:

  • Using CSRF tokens by defining a hidden input field holding the CSRF token value that only your website can generate and verify. This is the default option and is universally supported by all web browsers, however it requires using session cookies and a backend to store sessions.
  • Relying on Fetch metadata request headers to determine whether the request is a cross-site request or not. This option is only supported by modern browsers but does not require additional components to work.

Using CSRF tokens

Configuration

Any route configured with the web middleware group will automatically have CSRF protection enabled, through CSRF tokens.

If you want to configure a specific route with CSRF protection, you can use the ValidateCSRFToken middleware:

The ValidateCSRFToken middleware requires a session store to be configured. As such you should also add the LoadSession middleware to the route.

from expanse.session.middleware.load_session import LoadSession
from expanse.session.middleware.validate_csrf_token import ValidateCSRFToken

router.post('/profile', ...).middleware(LoadSession, ValidateCSRFToken)

Protecting forms

As mentioned earlier, you should include a hidden input field in your forms to hold the CSRF token value. This may be done using the csrf_token helper function inside your templated:

<form method="POST" action="/">
  <input type="hidden" name="_token" value="{{ csrf_token() }}">
  <input type="name" name="name" placeholder="Enter your name">
  <button type="submit">Submit</button>
</form>

During the form submission, the ValidateCSRFToken middleware will automatically validate the _token token, only allowing the form submissions with a valid CSRF token.

Handling errors

If the CSRF token validation fails, the ValidateCSRFToken middleware will throw a CSRFTokenMismatchError exception. By default, this exception will be caught by the framework and a 419 Page Expired response will be returned.

If you'd like to customize the error handling, you override the default error handler in your AppServiceProvider:

from expanse.contracts.debug.exception_handler import ExceptionHandler
from expanse.core.http.exceptions import HTTPException
from expanse.session.exceptions import CSRFTokenMismatchError
from expanse.support.service_provider import ServiceProvider


class AppServiceProvider(ServiceProvider):
    async def boot(self, handler: ExceptionHandler) -> None:
        handler.prepare_using(
            CSRFTokenMismatchError,
            self.handle_csrf_token_mismatch
        )

    def handle_csrf_token_mismatch(
        self, error: CSRFTokenMismatchError
    ) -> HTTPException:
        return HTTPException(403, str(error))

Using the X-CSRF-TOKEN header

In addition to checking for the CSRF token as a POST parameter for forms, the ValidateCSRFToken middleware will also check for a X-CSRF-TOKEN request header.

Using the X-XSRF-TOKEN header

Expanse stores the current CSRF token in an encrypted XSRF-TOKEN cookie that is included with each response generated by the framework. This cookie can be read by the frontend JavaScript and used to set the X-XSRF-TOKEN header on subsequent requests.

This allows frontend request libraries like Axios to read the XSRF-TOKEN automatically and set it as a X-XSRF-TOKEN header when making Ajax requests without server-rendered forms.

The XSRF-TOKEN cookie will be encrypted if — and only if — the EncryptCookies middleware is enabled for the route matching the request. This is the default behavior for the web middleware group.

Using Fetch metadata

Configuration

To enable CSRF protection using Fetch metadata headers, you need to use the CrossSiteProtection middleware.

You can replace the ValidateCSRFToken middleware in the web middleware group with the CrossSiteProtection middleware:

from expanse.core.http.middleware.middleware_stack import MiddlewareStack


async def configure_middleware(middleware: MiddlewareStack):
    from expanse.http.middleware.cross_site_protection import CrossSiteProtection
    from expanse.session.middleware.validate_csrf_token import ValidateCSRFToken

    middleware.group("web").replace(
        ValidateCSRFToken,
        CrossSiteProtection
    )

Or you can add the CrossSiteProtection middleware to specific routes:

from expanse.http.middleware.cross_site_protection import CrossSiteProtection

router.post('/profile', ...).middleware(CrossSiteProtection)

How it works

The CrossSiteProtection middleware will check the Fetch metadata headers to determine whether the request is a cross-site request or not. If it is, the middleware will reject the request with a 403 Forbidden response.

It works by doing the following, in order:

  • If the request method is considered "safe" (e.g. GET, HEAD, OPTIONS), the request is allowed.
  • If the Sec-Fetch-Site header is set to same-origin or none, the request is allowed.
  • If the request has an Origin header that matches the request's host, the request is allowed. This check relies on the Host header, so make sure to configure trusted hosts and trusted proxies.
  • If none of the above conditions are met, the request is considered a cross-site request and is rejected with a 403 Forbidden response.