fix: resolve 20 security vulnerabilities from comprehensive audit
Critical (1-3): - Replace hardcoded admin credentials with secure auto-generation (seed.py) - Enforce SECRET_KEY configuration, fail in production if missing (config.py) - Add Zip Slip and Zip Bomb protection to all ZIP import services High/Medium (4-9): - Add 50MB file size limit and extension whitelist to evidence uploads - Configure CORS origins via environment variable instead of hardcoded - Migrate JWT storage from localStorage to HttpOnly cookies (frontend+backend) - Add rate limiting (5/min) on login endpoint via slowapi - Replace generic dict payloads with Pydantic schemas (mass assignment) Medium (10-17): - Check is_active on login to prevent disabled users from authenticating - Sanitize exception messages in API responses (system, data_sources) - Escape LIKE wildcards in all ilike search filters across 8 routers - Run Docker container as non-root user (appuser) - Make MINIO_SECURE configurable via environment variable - Add password complexity policy (12+ chars, upper/lower/digit/special) - Implement JWT token revocation via in-memory blacklist + reduce TTL to 15min - Replace xml.etree with defusedxml to prevent Billion Laughs attacks Low (18-20): - Add security headers to Nginx (CSP, X-Frame-Options, HSTS-ready, etc.) - Disable Swagger UI/ReDoc/OpenAPI in production - Restrict /health endpoint to internal networks via Nginx ACL Also: rewrite install.sh as interactive wizard for guided deployment, fix test-from-template validation error (technique_id UUID vs MITRE ID)
This commit is contained in:
@@ -75,4 +75,4 @@ class TestTemplateInstantiate(BaseModel):
|
||||
"""Payload to create a real test from an existing template."""
|
||||
|
||||
template_id: uuid.UUID
|
||||
technique_id: uuid.UUID
|
||||
technique_id: str # accepts both UUID and MITRE ID (e.g. "T1059.001")
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
"""Pydantic schemas for User management endpoints."""
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, field_validator
|
||||
|
||||
|
||||
# ── Password policy ─────────────────────────────────────────────────
|
||||
|
||||
_MIN_PASSWORD_LENGTH = 12
|
||||
|
||||
_PASSWORD_RULES: list[tuple[str, str]] = [
|
||||
(r"[A-Z]", "at least one uppercase letter"),
|
||||
(r"[a-z]", "at least one lowercase letter"),
|
||||
(r"[0-9]", "at least one digit"),
|
||||
(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>/?`~]", "at least one special character"),
|
||||
]
|
||||
|
||||
|
||||
def _validate_password_strength(password: str) -> str:
|
||||
"""Check that *password* satisfies the complexity policy.
|
||||
|
||||
Rules:
|
||||
- Minimum 12 characters
|
||||
- At least one uppercase letter
|
||||
- At least one lowercase letter
|
||||
- At least one digit
|
||||
- At least one special character
|
||||
"""
|
||||
errors: list[str] = []
|
||||
|
||||
if len(password) < _MIN_PASSWORD_LENGTH:
|
||||
errors.append(f"must be at least {_MIN_PASSWORD_LENGTH} characters long")
|
||||
|
||||
for pattern, description in _PASSWORD_RULES:
|
||||
if not re.search(pattern, password):
|
||||
errors.append(description)
|
||||
|
||||
if errors:
|
||||
raise ValueError(
|
||||
"Password does not meet complexity requirements: " + "; ".join(errors)
|
||||
)
|
||||
|
||||
return password
|
||||
|
||||
|
||||
# ── Create ──────────────────────────────────────────────────────────
|
||||
@@ -16,6 +56,11 @@ class UserCreate(BaseModel):
|
||||
password: str
|
||||
role: str = "viewer"
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def password_strength(cls, v: str) -> str:
|
||||
return _validate_password_strength(v)
|
||||
|
||||
|
||||
# ── Update ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -28,6 +73,13 @@ class UserUpdate(BaseModel):
|
||||
is_active: bool | None = None
|
||||
password: str | None = None
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def password_strength(cls, v: str | None) -> str | None:
|
||||
if v is not None:
|
||||
return _validate_password_strength(v)
|
||||
return v
|
||||
|
||||
|
||||
# ── Read (full) ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user