security: fix 6 vulnerabilities identified in SDLC audit
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- fix(auth): enforce API key scopes in require_role/require_any_role;
  attach _api_key_scopes to user on API key auth; add require_scope()
  dependency — scopes were stored but never enforced (CWE-285)

- fix(sso): read SECURE_COOKIES env var for SSO cookie instead of
  hardcoded secure=False — SAML sessions now respect HTTPS config (CWE-614)

- fix(webhooks): SSRF prevention — validate webhook URLs against private
  and reserved CIDRs at creation/update time (CWE-918)

- fix(knowledge): restrict playbook/lesson create, update and restore
  to admin/red_lead/blue_lead roles — was open to any authenticated user (CWE-284)

- fix(alerts): restrict alert acknowledge/resolve/dismiss to admin/lead
  roles — any user could silence security alerts (CWE-284)

- security: delete get_admin_creds.py, check_auth.py, deploy.py scripts
  containing hardcoded root SSH credentials and production DB access;
  add scripts/.gitignore to prevent reintroduction (CWE-798)
This commit is contained in:
kitos
2026-05-22 09:46:29 +02:00
parent f36c633d16
commit 6f4901b611
7 changed files with 145 additions and 14 deletions

View File

@@ -1,8 +1,55 @@
"""Pydantic schemas for Webhook endpoints."""
import ipaddress
import socket
import uuid
from datetime import datetime
from typing import Any
from pydantic import BaseModel, HttpUrl, ConfigDict
from urllib.parse import urlparse
from pydantic import BaseModel, ConfigDict, field_validator
# RFC-5735 / RFC-1918 / RFC-3927 — ranges that must never be webhook targets
_BLOCKED_NETWORKS = [
ipaddress.ip_network("10.0.0.0/8"),
ipaddress.ip_network("172.16.0.0/12"),
ipaddress.ip_network("192.168.0.0/16"),
ipaddress.ip_network("169.254.0.0/16"), # link-local / AWS IMDS
ipaddress.ip_network("127.0.0.0/8"), # loopback
ipaddress.ip_network("::1/128"), # IPv6 loopback
ipaddress.ip_network("fc00::/7"), # IPv6 ULA
]
def _validate_webhook_url(url: str) -> str:
"""Reject URLs that point to private/reserved addresses (SSRF prevention)."""
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise ValueError("Webhook URL must use http or https")
hostname = parsed.hostname
if not hostname:
raise ValueError("Webhook URL must include a hostname")
# Resolve hostname to IP(s) and reject any private/reserved address
try:
infos = socket.getaddrinfo(hostname, None)
for info in infos:
raw_ip = info[4][0]
try:
ip_obj = ipaddress.ip_address(raw_ip)
except ValueError:
continue
for network in _BLOCKED_NETWORKS:
if ip_obj in network:
raise ValueError(
f"Webhook URL resolves to a private/reserved address ({raw_ip}) "
"and cannot be used"
)
except OSError:
# DNS resolution failure — allow (will fail at dispatch time)
pass
return url
class WebhookConfigCreate(BaseModel):
name: str
@@ -11,6 +58,12 @@ class WebhookConfigCreate(BaseModel):
events: list[str] = []
is_active: bool = True
@field_validator("url")
@classmethod
def url_must_be_external(cls, v: str) -> str:
return _validate_webhook_url(v)
class WebhookConfigUpdate(BaseModel):
name: str | None = None
url: str | None = None
@@ -18,6 +71,13 @@ class WebhookConfigUpdate(BaseModel):
events: list[str] | None = None
is_active: bool | None = None
@field_validator("url")
@classmethod
def url_must_be_external(cls, v: str | None) -> str | None:
if v is None:
return v
return _validate_webhook_url(v)
class WebhookConfigOut(BaseModel):
id: uuid.UUID
name: str