Authentication Security Audit
Last audit date: 2026-04-23 (phase 6 security hardening refresh)
Scope
This audit covers the authentication, authorization, and request-enforcement paths in MCP Hangar after the phase 6 security hardening work:
- API key and JWT/OIDC authentication
- Role-based access control (RBAC) and
policy:writeauthorization - Tool access policies (TAP)
- HTTP and WebSocket auth enforcement
- Browser-oriented CSRF defense-in-depth on session suspension
- Core package loading between
src/andsrc/mcp_hangar/
Current Security Posture
Authentication
- API requests rely on
request.state.auth; WebSocket / generic ASGI paths usescope["auth"]. - HTTP and WebSocket auth enforcement now share one core implementation in
src/mcp_hangar/server/api/middleware.py. - WebSocket connections support
?token=bearer mapping for clients that cannot set custom headers. - Trusted proxy resolution is centralized through
TrustedProxyResolver, preventing spoofedX-Forwarded-Forfrom untrusted peers.
Authorization
/api/agent/policyno longer trusts a magic internal header.- Policy push now requires an authenticated principal plus
policy:writeauthorization. - Failed policy pushes emit
PolicyPushRejectedaudit events. - The
agentrole includes the explicitpolicy:writepermission required by hangar-agent.
Browser CSRF Defense
- CSRF enforcement is intentionally scoped to browser-style session suspension requests.
POST /sessions/{session_id}/suspendrequiresX-Requested-Withonly when the request looks browser-originated (Origin,Referer, orCookiepresent).- API key clients, bearer-token clients, and non-browser API callers bypass the CSRF check.
- This keeps REST API automation compatible while still defending against browser-triggered session suspension.
WebSocket Security
- Authentication failures close the socket with code
1008before the connection is used. Originvalidation happens beforewebsocket.accept()to mitigate cross-site WebSocket hijacking.- Per-connection backpressure is enforced with bounded queues.
Enterprise Boundary
- Core bootstrap/router code no longer scatters direct
enterprise.*imports across server modules. - Optional integrations are resolved via
src/mcp_hangar/server/bootstrap/enterprise.py. - The boundary exposes provider hooks for:
- license validation
- auth CQRS registration
- API route extension
- enterprise-backed event store creation
- observability adapter creation
- legacy bootstrap compatibility exports
- Entry points are supported when available; the monorepo layout uses a controlled fallback loader so development remains functional without breaking the core boundary.
Findings Status
| Finding | Status | Notes |
|---|---|---|
| K-1 Agent policy auth bypass | Fixed | /api/agent/policy requires authenticated principal + policy:write; rejection events emitted |
| K-2 WebSocket auth / CSWSH gaps | Fixed | Shared auth enforcement, pre-accept Origin validation, bounded queue backpressure |
| K-3 Unsafe unauthenticated HTTP exposure | Fixed | Non-loopback HTTP bind blocked without auth unless explicitly overridden |
| K-4 CORS / host / CSRF hardening | Fixed | Explicit CORS config, TrustedHostMiddleware, browser-scoped CSRF defense |
| W-1/W-2 Header identity spoofing via proxies | Fixed | Trusted proxy resolution centralized and required for forwarded identity trust |
| W-3 SSRF on remote endpoints | Fixed | SSRF validation blocks private/link-local targets |
| W-4 Unbounded suspended-session cache | Fixed | TTL-bounded cache with max size |
| W-5 JWT algorithm confusion | Fixed | Mixed symmetric/asymmetric algorithm families rejected |
| A-5 Core importing enterprise directly | Fixed | Server bootstrap/router path moved behind single core enterprise boundary |
| A-7 Divergent HTTP/WS auth middleware | Fixed | Core shared auth middleware path now handles both |
Recommendations
| Item | Status | Notes |
|---|---|---|
| API key hash-only storage | Pass | Raw keys are not persisted |
| JWT algorithm-family validation | Pass | Mixed HS*/RS*/ES*/PS* families rejected |
| Trusted proxy validation | Pass | Only configured proxies may influence forwarded source identity |
| WebSocket origin validation | Pass | Performed before accept |
| Shared auth logic across protocols | Pass | One core implementation reduces drift |
| Core/enterprise import boundary | Pass | Centralized in server/bootstrap/enterprise.py |
| Repo-wide Ruff cleanliness | Follow-up | uv run ruff check src/ tests/ still reports historical unrelated test lint debt |
| Manual exploit verification | Pass | Replay confirmed K-1 returns 401, K-2 closes with 1008, K-3 aborts non-loopback startup with SystemExit(1), and K-4 rejects hostile hosts with 400 under strict CORS |
| TLS / mTLS at deployment edge | Manual | Must be enforced by deployment topology / reverse proxy |
Verification Evidence
uv run pytest tests/ -x -q-- passuv run mypy src/-- pass- Manual exploit replay of K-1..K-4 -- pass (
/agent/policy/spoof returns 401, unauthenticated WebSocket closes 1008, non-loopback no-auth HTTP exits 1, hostile Host rejected with 400) - Focused security and boundary suites cover:
tests/security/test_critical.pytests/security/test_identity_network.pytests/unit/test_bootstrap_enterprise_boundary.pytests/unit/test_bootstrap_enterprise_loading.pytests/unit/test_api_auth_enforcement.pytests/unit/test_agent_policy.pytests/unit/test_ws_auth.py
Open Items
- Repo-wide Ruff debt in historical tests remains outside the scope of this hardening pass.
- If enterprise packaging is split into a separately installed distribution later, the fallback loader can be retired in favor of entry-point-only discovery.