feat: enhance backend security and configuration
- Updated Dockerfile to improve security with a non-root user and added health checks. - Modified docker-compose.yml to set containers as read-only, restrict ports to localhost, and implement health checks. - Enhanced .env.example with additional environment variables for security and configuration. - Improved FastAPI application with middleware for security headers, CORS, and body size limits. - Refactored authentication flow in auth.py to include state validation and improved error handling. - Added rate limiting to various endpoints to prevent abuse. - Updated researcher and publication handling to ensure better validation and error management.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Middleware de cabeceras de seguridad HTTP.
|
||||
|
||||
Aplica un perfil seguro por defecto:
|
||||
- Strict-Transport-Security (HSTS) — fuerza HTTPS en navegadores compatibles.
|
||||
- X-Content-Type-Options: nosniff
|
||||
- X-Frame-Options: DENY (clickjacking)
|
||||
- Referrer-Policy: strict-origin-when-cross-origin
|
||||
- Permissions-Policy: bloquea APIs sensibles por defecto
|
||||
- Cross-Origin-Opener-Policy / Resource-Policy: aislamiento del navegador
|
||||
- Content-Security-Policy laxa para Swagger/OpenAPI (CDN), restrictiva para el resto.
|
||||
|
||||
NOTA: El frontend SPA tiene su propia CSP en su servidor. Aquí
|
||||
endurecemos lo que sirve el backend (JSON, XML, ZIP, /docs, /redoc, etc.).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from app.core.config import Settings
|
||||
|
||||
|
||||
_DOCS_PATHS = ("/docs", "/redoc", "/openapi.json")
|
||||
|
||||
_BASE_CSP = (
|
||||
"default-src 'none'; "
|
||||
"frame-ancestors 'none'; "
|
||||
"base-uri 'none'; "
|
||||
"form-action 'none'"
|
||||
)
|
||||
|
||||
_SWAGGER_CSP = (
|
||||
"default-src 'self'; "
|
||||
"img-src 'self' data: https://fastapi.tiangolo.com; "
|
||||
"script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; "
|
||||
"style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; "
|
||||
"font-src 'self' data: https://cdn.jsdelivr.net; "
|
||||
"connect-src 'self'; "
|
||||
"frame-ancestors 'none'; "
|
||||
"base-uri 'self'; "
|
||||
"form-action 'self'"
|
||||
)
|
||||
|
||||
|
||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Inserta cabeceras de seguridad en cada respuesta.
|
||||
"""
|
||||
|
||||
def __init__(self, app, settings: Settings):
|
||||
super().__init__(app)
|
||||
self._settings = settings
|
||||
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
response: Response = await call_next(request)
|
||||
|
||||
response.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||
response.headers.setdefault("X-Frame-Options", "DENY")
|
||||
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
response.headers.setdefault(
|
||||
"Permissions-Policy",
|
||||
"geolocation=(), microphone=(), camera=(), payment=(), usb=(), "
|
||||
"accelerometer=(), gyroscope=(), magnetometer=(), interest-cohort=()",
|
||||
)
|
||||
response.headers.setdefault("Cross-Origin-Opener-Policy", "same-origin")
|
||||
response.headers.setdefault("Cross-Origin-Resource-Policy", "same-site")
|
||||
response.headers.setdefault("X-Permitted-Cross-Domain-Policies", "none")
|
||||
|
||||
if request.url.path in _DOCS_PATHS:
|
||||
response.headers.setdefault("Content-Security-Policy", _SWAGGER_CSP)
|
||||
else:
|
||||
response.headers.setdefault("Content-Security-Policy", _BASE_CSP)
|
||||
|
||||
if request.url.scheme == "https" or self._settings.is_production:
|
||||
hsts = f"max-age={self._settings.SECURITY_HSTS_SECONDS}"
|
||||
if self._settings.SECURITY_HSTS_INCLUDE_SUBDOMAINS:
|
||||
hsts += "; includeSubDomains"
|
||||
if self._settings.SECURITY_HSTS_PRELOAD:
|
||||
hsts += "; preload"
|
||||
response.headers.setdefault("Strict-Transport-Security", hsts)
|
||||
|
||||
response.headers.pop("Server", None)
|
||||
response.headers.pop("X-Powered-By", None)
|
||||
|
||||
return response
|
||||
Reference in New Issue
Block a user