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:
2026-02-11 08:56:26 +01:00
parent e7e63161e8
commit 64d64080e0
36 changed files with 1154 additions and 311 deletions

View File

@@ -4,10 +4,13 @@ Security utilities: password hashing and JWT token management.
This module provides pure functions for:
- Hashing and verifying passwords using bcrypt via passlib.
- Creating JWT access tokens using python-jose.
- Managing an in-memory token blacklist for revocation.
No endpoints are defined here.
"""
import threading
import uuid as _uuid
from datetime import datetime, timedelta, timezone
from jose import jwt
@@ -38,13 +41,53 @@ def verify_password(plain: str, hashed: str) -> bool:
def create_access_token(data: dict) -> str:
"""Create a signed JWT containing *data* plus an ``exp`` claim.
"""Create a signed JWT containing *data* plus ``exp`` and ``jti`` claims.
The token expires after ``ACCESS_TOKEN_EXPIRE_MINUTES`` (from settings).
- ``jti`` (JWT ID): unique identifier that enables token revocation.
- ``exp``: expiration timestamp based on ``ACCESS_TOKEN_EXPIRE_MINUTES``.
"""
to_encode = data.copy()
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
)
to_encode.update({"exp": expire})
to_encode.update({
"exp": expire,
"jti": str(_uuid.uuid4()),
})
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
# ---------------------------------------------------------------------------
# Token blacklist (in-memory)
# ---------------------------------------------------------------------------
# Stores (jti, expiry_timestamp) tuples. Entries are automatically purged
# once they are past their original expiry (the token would be invalid
# anyway at that point). Thread-safe via a simple lock.
#
# For multi-worker / multi-process deployments, consider replacing this
# with a shared store like Redis.
# ---------------------------------------------------------------------------
_blacklist: dict[str, float] = {} # jti → expiry epoch
_blacklist_lock = threading.Lock()
def blacklist_token(jti: str, exp: float) -> None:
"""Add *jti* to the blacklist until it naturally expires at *exp*."""
with _blacklist_lock:
_blacklist[jti] = exp
_cleanup_blacklist()
def is_token_blacklisted(jti: str) -> bool:
"""Return ``True`` if *jti* has been revoked."""
with _blacklist_lock:
return jti in _blacklist
def _cleanup_blacklist() -> None:
"""Remove entries whose tokens have already expired (caller holds lock)."""
now = datetime.now(timezone.utc).timestamp()
expired = [k for k, exp in _blacklist.items() if exp < now]
for k in expired:
del _blacklist[k]