Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions mcpgateway/admin_ui/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
import htmx from 'htmx.org';
window.htmx = htmx;

// Configure HTMX to use CSP nonce for inline event handlers
// The nonce is set in the template via window.htmxConfig before this bundle loads
if (window.htmxConfig && window.htmxConfig.inlineScriptNonce) {
htmx.config.inlineScriptNonce = window.htmxConfig.inlineScriptNonce;
}

// Bootstrap MUST be first - initializes window.Admin before any modules run
import "./bootstrap.js";

Expand Down
19 changes: 19 additions & 0 deletions mcpgateway/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3368,6 +3368,25 @@ def tojson_attr(value: object) -> str:

jinja_env.filters["tojson_attr"] = tojson_attr


def get_csp_nonce_from_request(request: Request) -> str:
"""
Retrieve the CSP nonce from the request state.
Used in templates to add nonce attributes to inline scripts.

Args:
request: The FastAPI Request object. Can be None in test contexts.

Returns:
The CSP nonce string, or empty string if not available.
"""
if request is None:
return ""
return getattr(request.state, "csp_nonce", "")


jinja_env.globals["csp_nonce"] = get_csp_nonce_from_request

templates = Jinja2Templates(env=jinja_env)
if not settings.templates_auto_reload:
logger.info("🎨 Template auto-reload disabled (production mode)")
Expand Down
51 changes: 40 additions & 11 deletions mcpgateway/middleware/security_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@

This module implements essential security headers to prevent common attacks including
XSS, clickjacking, MIME sniffing, and cross-origin attacks.

The Content-Security-Policy (CSP) uses a nonce-based approach to allow legitimate
inline scripts while blocking malicious ones. Each request generates a unique
cryptographic nonce that must be included in inline script tags.
"""

# Standard
# Standard Library
import secrets

# Third-Party
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
Expand All @@ -31,9 +39,15 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
- X-Frame-Options: Prevents clickjacking attacks
- X-XSS-Protection: Disables legacy XSS protection (modern browsers use CSP)
- Referrer-Policy: Controls referrer information sent with requests
- Content-Security-Policy: Prevents XSS and other code injection attacks
- Content-Security-Policy: Nonce-based CSP prevents XSS and code injection
- Strict-Transport-Security: Forces HTTPS connections (when appropriate)

CSP Implementation:
- Uses cryptographically secure nonces (secrets.token_urlsafe(16))
- No unsafe-inline or unsafe-eval directives
- Nonce stored in request.state.csp_nonce for template access
- Inline scripts must include nonce="{{ csp_nonce(request) }}" attribute

Sensitive headers removed:
- X-Powered-By: Removes server technology disclosure
- Server: Removes server version information
Expand All @@ -42,17 +56,21 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
>>> middleware = SecurityHeadersMiddleware(None)
>>> isinstance(middleware, SecurityHeadersMiddleware)
True
>>> # Test CSP directive construction
>>> # Test CSP directive construction with nonce
>>> import secrets
>>> csp_nonce = secrets.token_urlsafe(16)
>>> csp_directives = [
... "default-src 'self'",
... "script-src 'self' 'unsafe-inline'",
... "style-src 'self' 'unsafe-inline'"
... f"script-src 'self' 'nonce-{csp_nonce}'",
... f"style-src 'self' 'nonce-{csp_nonce}'"
... ]
>>> csp = "; ".join(csp_directives) + ";"
>>> "default-src 'self'" in csp
True
>>> csp.endswith(";")
True
>>> "'nonce-" in csp
True
>>> # Test HSTS value construction
>>> hsts_max_age = 31536000
>>> hsts_value = f"max-age={hsts_max_age}"
Expand Down Expand Up @@ -116,11 +134,13 @@ async def dispatch(self, request: Request, call_next) -> Response:
>>> "strict-origin" in referrer_policy
True

Test CSP directive construction:
Test CSP directive construction with nonce-based approach:
>>> import secrets
>>> csp_nonce = secrets.token_urlsafe(16)
>>> csp_directives = [
... "default-src 'self'",
... "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com",
... "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com",
... f"script-src 'self' 'nonce-{csp_nonce}' https://cdnjs.cloudflare.com",
... f"style-src 'self' 'nonce-{csp_nonce}' https://cdnjs.cloudflare.com",
... "img-src 'self' data: https:",
... "font-src 'self' data: https://cdnjs.cloudflare.com",
... "connect-src 'self' ws: wss: https:",
Expand Down Expand Up @@ -245,6 +265,11 @@ async def dispatch(self, request: Request, call_next) -> Response:
>>> 'Vary' in resp.headers and 'Origin' in resp.headers['Vary']
True
"""
# Generate CSP nonce BEFORE processing request so templates can access it
# This must happen before call_next() so request.state.csp_nonce is available during template rendering
csp_nonce = secrets.token_urlsafe(16)
request.state.csp_nonce = csp_nonce

response = await call_next(request)

# Only apply security headers if enabled
Expand All @@ -271,12 +296,16 @@ async def dispatch(self, request: Request, call_next) -> Response:

response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"

# Content Security Policy
# This CSP is designed to work with the Admin UI while providing security
# Dynamically set frame-ancestors based on X_FRAME_OPTIONS setting to stay consistent
# Content Security Policy with nonce-based approach (nonce already generated above)

# CSP directives with nonce-based approach for scripts
# Note: style-src uses 'unsafe-inline' without nonce (nonce would disable unsafe-inline)
# This is needed for Alpine.js dynamic inline styles and is acceptable security trade-off
# 'unsafe-hashes' allows onclick/onload/etc inline event handlers
# Scripts still require nonces - no 'unsafe-inline' for script-src (pentesting compliance)
csp_directives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com",
f"script-src 'self' 'nonce-{csp_nonce}' 'unsafe-hashes' https://cdnjs.cloudflare.com https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com",
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net",
"img-src 'self' data: https:",
"font-src 'self' data: https://cdnjs.cloudflare.com",
Expand Down
Loading
Loading