Files
gh-christianlouis-inboxconv…/backend/app/core/middleware.py
T
copilot-swe-agent[bot] 27c50e49cf Fix all mypy and eslint errors, add copilot lint instructions
Backend (mypy - 56 errors fixed):
- database.py: Fix async generator return type to AsyncGenerator
- database_models.py: Add type annotations for SQLEnum columns
- middleware.py: Use explicit Optional for exempt_paths parameter
- mail_processor.py: Fix type narrowing in fetch_emails, add Dict type annotation for KNOWN_PROVIDERS
- users.py, auth.py, providers.py, tasks.py: Add type: ignore comments for SQLAlchemy Column assignment patterns

Frontend (eslint - 3 errors, 2 warnings fixed):
- login/page.tsx: Replace any with unknown + type narrowing, prefix unused vars with underscore
- register/page.tsx: Replace any with unknown + type narrowing

Add .github/copilot-instructions.md with lint-check requirements

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/dac7ab78-fe27-4fe4-890f-32c6c1c6d881
2026-03-23 10:48:16 +00:00

107 lines
3.8 KiB
Python

"""
Security middleware for adding security headers and CSRF protection.
"""
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp
import secrets
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Add security headers to all responses"""
async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
# Prevent clickjacking
response.headers["X-Frame-Options"] = "DENY"
# Prevent MIME type sniffing
response.headers["X-Content-Type-Options"] = "nosniff"
# Enable XSS protection (for older browsers)
response.headers["X-XSS-Protection"] = "1; mode=block"
# Strict Transport Security (HTTPS only)
# Note: Only enable in production with HTTPS
if request.url.hostname not in ["localhost", "127.0.0.1"]:
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
# Content Security Policy (adjust based on frontend needs)
csp = (
"default-src 'self'; "
"script-src 'self' 'unsafe-inline' https://js.stripe.com; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"font-src 'self' data:; "
"connect-src 'self' https://api.stripe.com; "
"frame-src https://js.stripe.com;"
)
response.headers["Content-Security-Policy"] = csp
# Referrer Policy
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# Permissions Policy (formerly Feature Policy)
response.headers["Permissions-Policy"] = (
"geolocation=(), microphone=(), camera=()"
)
return response
class CSRFProtectionMiddleware(BaseHTTPMiddleware):
"""
Basic CSRF protection for state-changing operations.
For API-only applications, this is less critical but still good practice.
"""
def __init__(self, app: ASGIApp, exempt_paths: list | None = None):
super().__init__(app)
self.exempt_paths = exempt_paths or [
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/google",
"/docs",
"/openapi.json",
"/health",
]
async def dispatch(self, request: Request, call_next) -> Response:
# Skip CSRF check for safe methods
if request.method in ["GET", "HEAD", "OPTIONS"]:
return await call_next(request)
# Skip CSRF check for exempt paths
if any(request.url.path.startswith(path) for path in self.exempt_paths):
return await call_next(request)
# For API endpoints using JWT, the token itself provides CSRF protection
# This is because attackers can't access the token stored in httpOnly cookies
# or local storage from a different origin
# If implementing cookie-based sessions, would check CSRF token here:
# csrf_token = request.headers.get("X-CSRF-Token")
# if not csrf_token or not self._validate_csrf_token(csrf_token):
# return JSONResponse(
# status_code=403,
# content={"detail": "CSRF token missing or invalid"}
# )
response = await call_next(request)
return response
@staticmethod
def _generate_csrf_token() -> str:
"""Generate a secure CSRF token"""
return secrets.token_urlsafe(32)
@staticmethod
def _validate_csrf_token(token: str) -> bool:
"""Validate CSRF token (implement actual validation logic)"""
# In a real implementation, compare against stored token
return len(token) == 43 # token_urlsafe(32) produces 43 chars