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:
@@ -20,6 +20,7 @@ Access Control
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import uuid as _uuid
|
||||
from typing import Optional
|
||||
|
||||
@@ -43,6 +44,29 @@ _RED_EDITABLE_STATES = (TestState.draft, TestState.red_executing)
|
||||
# States where blue evidence can be uploaded / deleted
|
||||
_BLUE_EDITABLE_STATES = (TestState.blue_evaluating,)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Upload safety limits
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Maximum upload size in bytes (default 50 MB)
|
||||
_MAX_UPLOAD_SIZE = 50 * 1024 * 1024
|
||||
|
||||
# Allowed file extensions (lowercase, with leading dot)
|
||||
_ALLOWED_EXTENSIONS: set[str] = {
|
||||
# Images / screenshots
|
||||
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg",
|
||||
# Documents
|
||||
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".csv", ".txt",
|
||||
".md", ".rtf", ".odt", ".ods",
|
||||
# Logs & captures
|
||||
".log", ".pcap", ".pcapng", ".evtx", ".json", ".xml",
|
||||
".yaml", ".yml", ".toml",
|
||||
# Archives (for bundled evidence)
|
||||
".zip", ".tar", ".gz", ".7z",
|
||||
# Other common evidence types
|
||||
".har", ".eml", ".msg",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
@@ -177,21 +201,39 @@ async def upload_evidence(
|
||||
# Validate permissions
|
||||
_validate_upload_permission(test, team, current_user)
|
||||
|
||||
# 1. Read content + hash
|
||||
content = await file.read()
|
||||
# 1. Validate file extension
|
||||
file_name = file.filename or "unnamed"
|
||||
_, ext = os.path.splitext(file_name)
|
||||
if ext.lower() not in _ALLOWED_EXTENSIONS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"File type '{ext}' is not allowed. "
|
||||
f"Permitted types: {', '.join(sorted(_ALLOWED_EXTENSIONS))}",
|
||||
)
|
||||
|
||||
# 2. Read content with size limit
|
||||
content = await file.read(_MAX_UPLOAD_SIZE + 1)
|
||||
if len(content) > _MAX_UPLOAD_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"File exceeds maximum upload size of "
|
||||
f"{_MAX_UPLOAD_SIZE // (1024 * 1024)} MB",
|
||||
)
|
||||
|
||||
# 3. Hash
|
||||
sha256 = hashlib.sha256(content).hexdigest()
|
||||
|
||||
# 2. Object key
|
||||
file_name = file.filename or "unnamed"
|
||||
key = f"{test_id}/{_uuid.uuid4()}_{file_name}"
|
||||
# 4. Object key (sanitise filename to prevent path traversal in storage)
|
||||
safe_name = os.path.basename(file_name)
|
||||
key = f"{test_id}/{_uuid.uuid4()}_{safe_name}"
|
||||
|
||||
# 3. Upload to MinIO
|
||||
# 5. Upload to MinIO
|
||||
upload_file(content, key)
|
||||
|
||||
# 4. Persist metadata
|
||||
# 6. Persist metadata
|
||||
evidence = Evidence(
|
||||
test_id=test_id,
|
||||
file_name=file_name,
|
||||
file_name=safe_name,
|
||||
file_path=key,
|
||||
sha256_hash=sha256,
|
||||
uploaded_by=current_user.id,
|
||||
@@ -202,7 +244,7 @@ async def upload_evidence(
|
||||
db.commit()
|
||||
db.refresh(evidence)
|
||||
|
||||
# 5. Audit
|
||||
# 7. Audit
|
||||
log_action(
|
||||
db,
|
||||
user_id=current_user.id,
|
||||
@@ -210,7 +252,7 @@ async def upload_evidence(
|
||||
entity_type="evidence",
|
||||
entity_id=evidence.id,
|
||||
details={
|
||||
"file_name": file_name,
|
||||
"file_name": safe_name,
|
||||
"sha256": sha256,
|
||||
"test_id": str(test_id),
|
||||
"team": team.value,
|
||||
|
||||
Reference in New Issue
Block a user