Skip to content

[SECURITY FEATURE]: Add Additional Configurable Security Headers to APIs for Admin UI #533

@crivetimihai

Description

@crivetimihai

Add Additional Configurable Security Headers to APIs for Admin UI and Static Assets

Summary: The nodejsscan static analysis identified 9 missing HTTP security headers in the MCP Gateway Admin UI and static assets. These headers are essential security best practices to mitigate client-side attacks (XSS, Clickjacking, MIME sniffing) and reduce information leakage.

Scope: Apply security headers for UI-related routes and static content:

  • Admin templates: mcpgateway/templates/admin.html, mcpgateway/templates/version_info_partial.html
  • Static files: mcpgateway/static/*.js, mcpgateway/static/*.css
  • Admin routes: /admin, /version (when format=html)
  • Static route: /static/*
  • Cookie security in: mcpgateway/admin.py

Implementation Details:

1. Create Security Headers Middleware

Add a new middleware class in mcpgateway/main.py after the existing middleware classes (around line 386):

class SecurityHeadersMiddleware(BaseHTTPMiddleware):
    """Add security headers to responses for admin UI and static assets."""
    
    async def dispatch(self, request: Request, call_next):
        response = await call_next(request)
        
        # Only apply to admin UI routes and static files when UI is enabled
        if settings.mcpgateway_ui_enabled and settings.security_headers_enabled:
            path = request.url.path
            if (path.startswith("/admin") or 
                path.startswith("/static") or 
                (path == "/version" and "html" in request.headers.get("accept", "")) or
                (path == "/version" and request.url.query and "format=html" in request.url.query)):
                
                # Content Security Policy
                if settings.csp_enabled:
                    csp_header_name = "Content-Security-Policy-Report-Only" if settings.csp_report_only else "Content-Security-Policy"
                    response.headers[csp_header_name] = settings.csp_header_value
                
                # X-Frame-Options - prevent clickjacking
                if settings.x_frame_options:
                    response.headers["X-Frame-Options"] = settings.x_frame_options
                
                # Strict-Transport-Security - enforce HTTPS (only in production)
                if settings.hsts_enabled and not settings.debug:
                    hsts_value = settings.hsts_header_value
                    if hsts_value:
                        response.headers["Strict-Transport-Security"] = hsts_value
                
                # X-XSS-Protection - legacy XSS protection
                if settings.x_xss_protection_enabled:
                    response.headers["X-XSS-Protection"] = "1; mode=block"
                
                # X-Content-Type-Options - prevent MIME sniffing
                if settings.x_content_type_options_enabled:
                    response.headers["X-Content-Type-Options"] = "nosniff"
                
                # X-Download-Options - prevent IE from executing downloads
                if settings.x_download_options_enabled:
                    response.headers["X-Download-Options"] = "noopen"
                
                # Remove server identification headers
                if settings.remove_server_headers:
                    response.headers.pop("X-Powered-By", None)
                    response.headers.pop("Server", None)
                
        return response

2. Register Middleware

Add the middleware registration in main.py after the existing middleware setup (around line 430):

# Add security headers middleware (after CORS middleware)
app.add_middleware(SecurityHeadersMiddleware)

3. Update Cookie Settings in admin.py

Modify the JWT cookie setting in mcpgateway/admin.py in the admin_ui() function (around line 1730):

# BEFORE (line ~1730):
response.set_cookie(key="jwt_token", value=jwt_token, httponly=True, secure=False, samesite="Strict")  # JavaScript CAN'T read it  # only over HTTPS  # or "Lax" per your needs

# AFTER:
response.set_cookie(
    key="jwt_token",
    value=jwt_token,
    httponly=True,  # Prevent JavaScript access
    secure=not settings.debug,  # Use secure cookies in production (using existing debug setting)
    samesite="lax",  # CSRF protection
    max_age=settings.token_expiry * 60  # Convert minutes to seconds
)

Also check for any other cookie settings in the file (search for set_cookie in admin.py) and apply the same security attributes.

4. CSP Considerations for HTMX and Alpine.js

The Admin UI uses:

  • HTMX from unpkg.com CDN
  • Alpine.js from cdn.jsdelivr.net CDN
  • Inline scripts for Alpine.js initialization
  • Inline styles for UI components

The CSP policy above allows these while maintaining security. In future iterations:

  • Consider moving inline scripts to external files
  • Use `nonces`` for necessary inline scripts
  • Bundle dependencies locally to remove CDN requirements

Additional CDNs Found in Implementation:

  • Tailwind CSS from cdn.tailwindcss.com
  • CodeMirror themes and modes from cdnjs.cloudflare.com
  • Chart.js from cdn.jsdelivr.net

Files to Modify:

  1. mcpgateway/main.py - Add SecurityHeadersMiddleware class and register it (after line 386)
  2. mcpgateway/admin.py - Update cookie settings with security attributes (line ~1730)
  3. mcpgateway/config.py - Add security headers configuration to Settings class (around line 270)
  4. .env.example - Add security headers environment variables

5. Environment-Based Configuration

Add environment variables for header configuration in config.py. Add these to the Settings class (around line 270, after the existing security settings):

# Security Headers Configuration
security_headers_enabled: bool = True
csp_enabled: bool = True
csp_report_uri: Optional[str] = None
csp_report_only: bool = False  # Set True to test CSP without blocking
hsts_enabled: bool = True
hsts_max_age: int = 31536000  # 1 year in seconds
hsts_include_subdomains: bool = True
hsts_preload: bool = False
x_frame_options: str = "DENY"  # DENY, SAMEORIGIN, or ALLOW-FROM uri
x_content_type_options_enabled: bool = True
x_xss_protection_enabled: bool = True
x_download_options_enabled: bool = True
remove_server_headers: bool = True  # Remove X-Powered-By and Server headers

# CSP Directives Configuration (can be customized via env)
csp_default_src: List[str] = ["'self'"]
csp_script_src: List[str] = [
    "'self'",
    "'unsafe-inline'",
    "'unsafe-eval'",
    "https://unpkg.com",
    "https://cdn.jsdelivr.net",
    "https://cdn.tailwindcss.com",
    "https://cdnjs.cloudflare.com"
]
csp_style_src: List[str] = [
    "'self'",
    "'unsafe-inline'",
    "https://unpkg.com",
    "https://cdn.jsdelivr.net",
    "https://cdnjs.cloudflare.com"
]
csp_img_src: List[str] = ["'self'", "data:", "https:"]
csp_font_src: List[str] = ["'self'", "data:", "https://cdnjs.cloudflare.com"]
csp_connect_src: List[str] = ["'self'"]
csp_frame_ancestors: List[str] = ["'none'"]

@property
def csp_header_value(self) -> str:
    """Generate the Content-Security-Policy header value from configuration."""
    if not self.csp_enabled:
        return ""
    
    directives = []
    
    # Build CSP directives from configuration
    if self.csp_default_src:
        directives.append(f"default-src {' '.join(self.csp_default_src)}")
    if self.csp_script_src:
        directives.append(f"script-src {' '.join(self.csp_script_src)}")
    if self.csp_style_src:
        directives.append(f"style-src {' '.join(self.csp_style_src)}")
    if self.csp_img_src:
        directives.append(f"img-src {' '.join(self.csp_img_src)}")
    if self.csp_font_src:
        directives.append(f"font-src {' '.join(self.csp_font_src)}")
    if self.csp_connect_src:
        directives.append(f"connect-src {' '.join(self.csp_connect_src)}")
    if self.csp_frame_ancestors:
        directives.append(f"frame-ancestors {' '.join(self.csp_frame_ancestors)}")
    
    # Add report-uri if configured
    if self.csp_report_uri:
        directives.append(f"report-uri {self.csp_report_uri}")
    
    return "; ".join(directives) + ";"

@property
def hsts_header_value(self) -> str:
    """Generate the Strict-Transport-Security header value from configuration."""
    if not self.hsts_enabled or self.debug:
        return ""
    
    value = f"max-age={self.hsts_max_age}"
    if self.hsts_include_subdomains:
        value += "; includeSubDomains"
    if self.hsts_preload:
        value += "; preload"
    return value

6. Update .env.example

Add these new configuration options to your .env.example file (add after the existing Security and CORS section):

#####################################
# Security Headers Configuration
#####################################

# Enable security headers middleware (true/false)
SECURITY_HEADERS_ENABLED=true

# Content Security Policy settings
CSP_ENABLED=true
CSP_REPORT_URI=
CSP_REPORT_ONLY=false

# HSTS (HTTP Strict Transport Security) settings
HSTS_ENABLED=true
HSTS_MAX_AGE=31536000  # 1 year in seconds
HSTS_INCLUDE_SUBDOMAINS=true
HSTS_PRELOAD=false

# X-Frame-Options setting (DENY, SAMEORIGIN, or ALLOW-FROM uri)
X_FRAME_OPTIONS=DENY

# Other security headers (true/false)
X_CONTENT_TYPE_OPTIONS_ENABLED=true
X_XSS_PROTECTION_ENABLED=true
X_DOWNLOAD_OPTIONS_ENABLED=true
REMOVE_SERVER_HEADERS=true

# CSP directive customization (JSON arrays)
# Uncomment and modify these to customize CSP directives
# CSP_DEFAULT_SRC='["self"]'
# CSP_SCRIPT_SRC='["self", "unsafe-inline", "unsafe-eval", "https://unpkg.com", "https://cdn.jsdelivr.net", "https://cdn.tailwindcss.com", "https://cdnjs.cloudflare.com"]'
# CSP_STYLE_SRC='["self", "unsafe-inline", "https://unpkg.com", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"]'
# CSP_IMG_SRC='["self", "data:", "https:"]'
# CSP_FONT_SRC='["self", "data:", "https://cdnjs.cloudflare.com"]'
# CSP_CONNECT_SRC='["self"]'
# CSP_FRAME_ANCESTORS='["none"]'

Action Items:

  • Add Content-Security-Policy (CSP) header with appropriate directives for HTMX/Alpine.js
  • Add X-Frame-Options (XFO) header set to DENY
  • Add Strict-Transport-Security (HSTS) header (production only)
  • Add X-XSS-Protection header with mode=block
  • Add X-Content-Type-Options header set to nosniff
  • Add X-Download-Options header set to noopen
  • Set cookies with HttpOnly, Secure, and SameSite attributes
  • Remove X-Powered-By header
  • Skip Public-Key-Pins (HPKP) - deprecated and not recommended

Testing Checklist:

  • Verify Admin UI still functions with CSP policy
  • Test HTMX requests work properly
  • Ensure Alpine.js components initialize
  • Check static assets load correctly
  • Validate headers using browser dev tools
  • Test with security scanning tools
  • Verify Tailwind CSS loads properly
  • Check CodeMirror editors function
  • Ensure Chart.js visualizations work

Notes:

  • Headers only apply when MCPGATEWAY_UI_ENABLED=true
  • CSP policy allows CDN resources required by Admin UI (HTMX from unpkg.com, Alpine.js from cdn.jsdelivr.net)
  • HSTS only enabled in production (when DEBUG_MODE=false or settings.debug_mode=False)
  • Cookie security enhanced with HttpOnly, Secure (production only), and SameSite=lax
  • The existing cookie in admin.py already has httponly=True but needs secure to be conditional on debug mode
  • Consider using fastapi-csp package for more advanced CSP management
  • Future enhancement: Add CSP nonces for inline scripts

References:

Priority: Medium — While the Admin UI is intended for development use, implementing these headers:

  • Establishes security best practices
  • Prepares for potential production exposure
  • Reduces security scanner noise
  • Protects against common web vulnerabilities

Suggested Milestone: v0.4.0 as part of general security hardening

Metadata

Metadata

Assignees

Labels

choreLinting, formatting, dependency hygiene, or project maintenance chorescicdIssue with CI/CD process (GitHub Actions, scaffolding)devopsDevOps activities (containers, automation, deployment, makefiles, etc)enhancementNew feature or requestfrontendFrontend development (HTML, CSS, JavaScript)pythonPython / backend development (FastAPI)securityImproves securitytriageIssues / Features awaiting triage

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions