CSRF Protection¶
If your site logs users in with a cookie, any other website can make a visitor's browser post to your forms, and the browser sends the cookie along. That's cross-site request forgery (CSRF). The defense is a secret token: your form carries it in a hidden field, and your route refuses any submission that doesn't send it back.
pydantic-schemaforms renders the hidden field. Your app creates the token, keeps it in the session and checks it, because only your app knows its sessions.
import hmac
import secrets
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from starlette.middleware.sessions import SessionMiddleware
from pydantic_schemaforms import Field, FormModel, html, render_form_html_async
class TransferForm(FormModel):
to_account: str = Field(title='To account')
amount: int = Field(title='Amount', ge=1)
app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key='change-me') # load from your settings
def issue_csrf_token(request: Request) -> str:
token = secrets.token_urlsafe(32)
request.session['csrf_token'] = token
return token
def csrf_token_is_valid(request: Request, submitted: object) -> bool:
expected = request.session.get('csrf_token')
return bool(expected and isinstance(submitted, str)) and hmac.compare_digest(
expected, submitted
)
def form_options(request: Request) -> dict:
return {
'submit_url': '/transfer',
'csrf_mode': 'required-provider',
'csrf_token_provider': issue_csrf_token(request),
'include_framework_assets': True,
'asset_mode': 'cdn',
}
@app.get('/transfer', response_class=HTMLResponse)
async def show_transfer(request: Request):
return await render_form_html_async(TransferForm, **form_options(request))
@app.post('/transfer', response_class=HTMLResponse)
async def submit_transfer(request: Request):
form = await request.form()
if not csrf_token_is_valid(request, form.get('csrf_token')):
raise HTTPException(status_code=403, detail='Invalid or expired form; reload the page.')
result = TransferForm.validate(form, **form_options(request))
if not result.is_valid:
return await result.render_with_errors_async()
amount = result.data['amount']
return html(t'<p>Transferred {amount}.</p>')
import hmac
import secrets
from flask import Flask, abort, request, session
from pydantic_schemaforms import Field, FormModel, html, render_form_html
class TransferForm(FormModel):
to_account: str = Field(title='To account')
amount: int = Field(title='Amount', ge=1)
app = Flask(__name__)
app.secret_key = 'change-me' # load from your settings
def issue_csrf_token() -> str:
token = secrets.token_urlsafe(32)
session['csrf_token'] = token
return token
def csrf_token_is_valid(submitted: object) -> bool:
expected = session.get('csrf_token')
return bool(expected and isinstance(submitted, str)) and hmac.compare_digest(
expected, submitted
)
def form_options() -> dict:
return {
'submit_url': '/transfer',
'csrf_mode': 'required-provider',
'csrf_token_provider': issue_csrf_token(),
'include_framework_assets': True,
'asset_mode': 'cdn',
}
@app.get('/transfer')
def show_transfer():
return render_form_html(TransferForm, **form_options())
@app.post('/transfer')
def submit_transfer():
if not csrf_token_is_valid(request.form.get('csrf_token')):
abort(403, 'Invalid or expired form; reload the page.')
result = TransferForm.validate(request.form, **form_options())
if not result.is_valid:
return result.render_with_errors()
amount = result.data['amount']
return html(t'<p>Transferred {amount}.</p>')
How it works¶
issue_csrf_token()makes a random token and stores it in the session.csrf_mode="required-provider"withcsrf_token_provider=renders it as a hiddencsrf_tokenfield. Therequired-providermode refuses to render without a token, so a missing one is caught at once instead of silently shipping an unprotected form.- The POST route checks the token first, with
hmac.compare_digestso the comparison takes the same time whatever the input, and answers 403 when it doesn't match. validate()gets the same options, so a re-rendered form carries a new token. The token isn't a model field, so it never appears inresult.data.
When you need it¶
You need CSRF protection for forms that change something (create, update,
delete, buy) on a site that identifies users by cookie. You usually don't for
APIs that authenticate with an Authorization header instead of cookies, or
for read-only pages.
Checklist¶
- Keep the session secret (
secret_key) out of your code, and long and random. - Serve the site over HTTPS.
- Never log the token values.
- Use
required-provider;csrf_field_name=changes the hidden field's name if your app already uses a different one.
Other CSRF modes
csrf_mode also accepts "off" (the default: no hidden field) and
"field-only", which renders the field without requiring a provider. It
exists for debugging and is only allowed together with debug=True.
CSRFMode.REQUIRED_PROVIDER and the other enum values work the same as
the strings.
That's the end of the tutorial. You can now build, validate, arrange, style and protect forms. From here:
- the Recipes have complete forms to copy;
- the Advanced User Guide covers live validation, data tables, JSON APIs and more;
- the Reference lists every input, layout and option.