diff --git a/backend/src/app.py b/backend/src/app.py index 4f0c4a6e..87d30533 100755 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -19,6 +19,7 @@ import asyncio from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware @@ -82,6 +83,12 @@ def ensure_initial_admin_user() -> None: "INITIAL_ADMIN_CREATE is enabled but INITIAL_ADMIN_USERNAME/INITIAL_ADMIN_PASSWORD is missing; skipping bootstrap." ) return + # Warn about env-var password — visible via /proc to other processes + logger.warning( + "INITIAL_ADMIN_PASSWORD is set via environment variable. " + "This is visible to other processes on the same host. " + "Consider removing the env var after bootstrap." + ) db = AuthSessionLocal() try: admin_role = db.query(Role).filter(Role.name == "Admin").first() @@ -182,6 +189,12 @@ app.add_middleware( allow_methods=["*"], allow_headers=["*"], ) + +# HSTS — force HTTPS in production (see [SEC:L-2]) +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=_allowed_origins if _allowed_origins else ["*"], +) # #endregion app_middleware # #region network_error_handler [C:1] [TYPE Function] # @BRIEF Global exception handler for NetworkError. diff --git a/backend/src/schemas/auth.py b/backend/src/schemas/auth.py index b71312dd..23706f55 100644 --- a/backend/src/schemas/auth.py +++ b/backend/src/schemas/auth.py @@ -7,6 +7,7 @@ # @INVARIANT Sensitive fields like password must not be included in response schemas. # @DATA_CONTRACT AuthPayload -> AuthSchema +import re from datetime import datetime from pydantic import BaseModel, EmailStr, Field, field_validator @@ -103,6 +104,19 @@ class ADGroupMappingCreate(BaseModel): ad_group: str role_id: str + @field_validator("ad_group") + @classmethod + def validate_ad_group(cls, v: str) -> str: + if not v or not v.strip(): + raise ValueError("AD group name must not be empty") + # Allow DOMAIN\groupname or distinguishedName (CN=...,DC=...) formats + if not re.match(r'^[A-Za-z0-9_.@()=\\\\-]+$', v): + raise ValueError( + "AD group name contains invalid characters. " + "Use format: DOMAIN\\groupname or CN=groupname,DC=domain" + ) + return v.strip() + # #endregion ADGroupMappingCreate