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-Siteheader is set tosame-originornone, the request is allowed. - If the request has an
Originheader that matches the request's host, the request is allowed. This check relies on theHostheader, 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 Forbiddenresponse.