security: HSTS, AD group validation, INITIAL_ADMIN_PASSWORD warning

L-2: HSTS middleware via TrustedHostMiddleware — restricts allowed hosts
  to ALLOWED_ORIGINS list, prevents Host header injection.

L-4: AD group name validation — ADGroupMappingCreate.ad_group validated
  with regex: DOMAIN\groupname or CN=...,DC=... format. Empty or
  invalid characters rejected.

L-5: INITIAL_ADMIN_PASSWORD warning — log warning that env-var password
  is visible via /proc to other processes on the same host.
This commit is contained in:
2026-05-26 15:20:29 +03:00
parent 2bc8ad87e1
commit ee4ca3cdf6
2 changed files with 27 additions and 0 deletions

View File

@@ -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.

View File

@@ -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