Skip to main content
Framefox provides automatic CSRF (Cross-Site Request Forgery) protection for form submissions. CSRF tokens are generated for each session and validated on POST, PUT, and PATCH requests to prevent unauthorized form submissions.

How CSRF Protection Works

CSRF attacks trick authenticated users into submitting malicious requests. Framefox protects against this by:
  1. Generating a unique token for each user session
  2. Storing the token in a secure cookie
  3. Requiring the token to be submitted with forms
  4. Validating tokens using constant-time comparison
  5. Rejecting requests with invalid or missing tokens

Using CSRF Tokens in Forms

Basic Form Protection

Add the csrf_token() function to your form templates:
<form method="POST" action="/update-profile">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    
    <label for="name">Name:</label>
    <input type="text" name="name" id="name" required>
    
    <label for="email">Email:</label>
    <input type="email" name="email" id="email" required>
    
    <button type="submit">Update Profile</button>
</form>

Login Forms

Login forms must include CSRF tokens:
<form method="POST" action="/login">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    
    <label for="email">Email:</label>
    <input type="email" name="_username" id="email" required>
    
    <label for="password">Password:</label>
    <input type="password" name="_password" id="password" required>
    
    <button type="submit">Login</button>
</form>

AJAX Requests

For AJAX requests, include the token in request headers or body:
// Get CSRF token from cookie
function getCsrfToken() {
    const cookies = document.cookie.split(';');
    for (let cookie of cookies) {
        const [name, value] = cookie.trim().split('=');
        if (name === 'csrf_token') {
            return value;
        }
    }
    return null;
}

// Include in fetch request
fetch('/api/update-user', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': getCsrfToken()
    },
    body: JSON.stringify({ name: 'John Doe' })
});

Alternative: Meta Tag

Store the token in a meta tag for easy access:
<!-- In your base template -->
<head>
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>

<script>
// Access token from meta tag
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;

fetch('/api/endpoint', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': csrfToken
    },
    body: JSON.stringify(data)
});
</script>

CSRF Token Generation

Tokens are automatically generated by the CsrfTokenManager:
import secrets

class CsrfTokenManager:
    def generate_token(self) -> str:
        """Generate a secure random token."""
        return secrets.token_urlsafe(32)
    
    def store_token(self, response, token: str):
        """Store token in secure cookie."""
        response.set_cookie(
            key="csrf_token",
            value=token,
            httponly=True,
            secure=True,  # HTTPS only
            samesite="strict"
        )

Token Validation

The CsrfTokenBadge class validates tokens using constant-time comparison:
import secrets
from fastapi import Request
from framefox.core.security.exceptions.invalid_csrf_token_exception import InvalidCsrfTokenException

class CsrfTokenBadge:
    def __init__(self, csrf_token: str):
        self.token = csrf_token
    
    def validate_csrf_token(self, request: Request):
        """Validate CSRF token using constant-time comparison."""
        stored_token = request.cookies.get("csrf_token", "")
        
        # Use constant-time comparison to prevent timing attacks
        if not secrets.compare_digest(self.token, stored_token):
            raise InvalidCsrfTokenException()
        
        return True
Reference: /home/daytona/workspace/source/framefox/core/security/passport/csrf_token_badge.py:18

Why Constant-Time Comparison?

The secrets.compare_digest() function prevents timing attacks by ensuring the comparison takes the same time regardless of where the difference occurs:
# Bad - vulnerable to timing attacks
if token1 == token2:
    return True

# Good - constant-time comparison
if secrets.compare_digest(token1, token2):
    return True

Automatic Validation in Firewall

The FirewallHandler automatically validates CSRF tokens:
async def _handle_form_authentication(
    self,
    request: Request,
    authenticator: AuthenticatorInterface,
    firewall_config: Dict,
    firewall_name: str,
) -> Response:
    login_path = firewall_config.get("login_path")
    
    # Skip CSRF validation for JWT authenticators
    if not self.utils.is_jwt_authenticator(authenticator):
        # Validate CSRF token with timing attack protection
        csrf_valid = await self.timing_protector.protected_csrf_validation(
            self.csrf_manager.validate_token, 
            request
        )
        
        if not csrf_valid:
            self.logger.error("CSRF validation failed")
            if hasattr(request.state, "session_id"):
                self.security_context_handler.set_authentication_error(
                    "A security error occurred. Please try again."
                )
            return RedirectResponse(url=login_path, status_code=303)
    
    return await self.handle_post_request(
        request, authenticator, firewall_config, firewall_name
    )
Reference: /home/daytona/workspace/source/framefox/core/security/handlers/firewall_handler.py:273

CSRF Token Lifecycle

1. Token Generation

Tokens are generated when:
  • A new session is created
  • User first visits the site
  • After successful login

2. Token Storage

Tokens are stored in:
  • Secure HTTP-only cookies
  • Session storage
  • Request state (during validation)

3. Token Validation

Tokens are validated on:
  • POST requests (form submissions)
  • PUT requests (updates)
  • PATCH requests (partial updates)
  • DELETE requests (optional)

4. Token Expiration

Tokens expire when:
  • User logs out
  • Session expires
  • Cookie is deleted

Handling CSRF Errors

Custom Error Messages

from framefox.core.security.exceptions.invalid_csrf_token_exception import InvalidCsrfTokenException

@app.exception_handler(InvalidCsrfTokenException)
async def csrf_exception_handler(request: Request, exc: InvalidCsrfTokenException):
    return templates.TemplateResponse(
        "error/csrf.html",
        {
            "request": request,
            "message": "Your session has expired. Please refresh and try again."
        },
        status_code=403
    )

Error Template

<!-- templates/error/csrf.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Security Error</title>
</head>
<body>
    <h1>Security Error</h1>
    <p>{{ message }}</p>
    <p>
        <a href="javascript:history.back()">Go Back</a> |
        <a href="/">Home Page</a>
    </p>
</body>
</html>

Configuration

Configure CSRF cookie behavior:
# config/security.yaml or application settings
csrf:
  cookie_name: csrf_token
  cookie_httponly: true
  cookie_secure: true  # Require HTTPS
  cookie_samesite: strict  # or 'lax'
  token_length: 32

Excluding Routes

Exclude certain routes from CSRF protection:
security:
  csrf:
    excluded_paths:
      - ^/api/webhook  # External webhooks
      - ^/api/callback  # OAuth callbacks

Advanced Usage

Manual Token Validation

For custom controllers, validate tokens manually:
from fastapi import Request, HTTPException
from framefox.core.request.csrf_token_manager import CsrfTokenManager

class CustomController:
    def __init__(self, csrf_manager: CsrfTokenManager):
        self.csrf_manager = csrf_manager
    
    async def update_settings(self, request: Request):
        # Validate CSRF token
        if not await self.csrf_manager.validate_token(request):
            raise HTTPException(
                status_code=403,
                detail="CSRF token validation failed"
            )
        
        # Process request
        form_data = await request.form()
        # ... update settings

Token Refresh

Refresh tokens after sensitive operations:
from framefox.core.request.csrf_token_manager import CsrfTokenManager

async def change_password(request: Request, csrf_manager: CsrfTokenManager):
    # Process password change
    # ...
    
    # Generate new CSRF token
    new_token = csrf_manager.generate_token()
    
    # Return response with new token
    response = RedirectResponse(url="/profile", status_code=303)
    csrf_manager.store_token(response, new_token)
    
    return response

Security Best Practices

1. Always Use HTTPS

CSRF tokens should only be transmitted over HTTPS:
csrf:
  cookie_secure: true  # Enforce HTTPS

2. Use SameSite Cookies

Set SameSite attribute to prevent cross-site token leakage:
response.set_cookie(
    key="csrf_token",
    value=token,
    samesite="strict"  # or 'lax' for some cross-site scenarios
)

3. Rotate Tokens Regularly

Rotate tokens after:
  • Login/logout
  • Password changes
  • Permission changes

4. Never Expose Tokens in URLs

# Bad - token in URL
<form method="GET" action="/update?csrf_token={{ csrf_token() }}">

# Good - token in hidden field
<form method="POST" action="/update">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

5. Validate on All State-Changing Requests

Protect all operations that modify data:
  • POST (create)
  • PUT (update)
  • PATCH (partial update)
  • DELETE (remove)

Testing CSRF Protection

Unit Tests

import pytest
from fastapi.testclient import TestClient

def test_csrf_protection(client: TestClient):
    # Request without CSRF token should fail
    response = client.post("/update-profile", data={
        "name": "John Doe"
    })
    assert response.status_code == 403
    
    # Request with valid token should succeed
    # First get the token
    response = client.get("/profile")
    csrf_token = response.cookies.get("csrf_token")
    
    # Submit form with token
    response = client.post("/update-profile", data={
        "name": "John Doe",
        "csrf_token": csrf_token
    })
    assert response.status_code == 200

Integration Tests

def test_csrf_flow(client: TestClient):
    # Login
    response = client.get("/login")
    csrf_token = response.cookies.get("csrf_token")
    
    # Submit login with token
    response = client.post("/login", data={
        "_username": "[email protected]",
        "_password": "password",
        "csrf_token": csrf_token
    })
    
    assert response.status_code == 303
    assert "access_token" in response.cookies

Troubleshooting

Token Mismatch Errors

If you’re getting CSRF errors:
  1. Check cookie settings: Ensure cookies are being set correctly
  2. Verify HTTPS: Secure cookies require HTTPS
  3. Check SameSite: Some browsers block cookies with strict SameSite
  4. Clear old sessions: Old tokens may be cached

AJAX Requests Failing

  1. Include token in headers: Add X-CSRF-Token header
  2. Check cookie access: Ensure JavaScript can read cookies
  3. Verify CORS settings: Cross-origin requests need proper CORS headers

Next Steps

Authentication

Learn about user authentication flows

Security Overview

Explore all security features

Build docs developers (and LLMs) love