Skip to main content
Glama

MaverickMCP

by wshobson
MIT License
165
  • Apple
test_security_headers.py19.9 kB
""" Comprehensive Security Headers Tests for Maverick MCP. Tests security headers configuration, middleware implementation, environment-specific headers, and CSP/HSTS policies. """ import os from unittest.mock import MagicMock, patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from maverick_mcp.api.middleware.security import ( SecurityHeadersMiddleware as APISecurityHeadersMiddleware, ) from maverick_mcp.config.security import ( SecurityConfig, SecurityHeadersConfig, ) from maverick_mcp.config.security_utils import ( SecurityHeadersMiddleware, apply_security_headers_to_fastapi, ) class TestSecurityHeadersConfig: """Test security headers configuration.""" def test_security_headers_default_values(self): """Test security headers have secure default values.""" config = SecurityHeadersConfig() assert config.x_content_type_options == "nosniff" assert config.x_frame_options == "DENY" assert config.x_xss_protection == "1; mode=block" assert config.referrer_policy == "strict-origin-when-cross-origin" assert "geolocation=()" in config.permissions_policy def test_hsts_header_generation(self): """Test HSTS header value generation.""" config = SecurityHeadersConfig() hsts_header = config.hsts_header_value assert f"max-age={config.hsts_max_age}" in hsts_header assert "includeSubDomains" in hsts_header assert "preload" not in hsts_header # Default is False def test_hsts_header_with_preload(self): """Test HSTS header with preload enabled.""" config = SecurityHeadersConfig(hsts_preload=True) hsts_header = config.hsts_header_value assert "preload" in hsts_header def test_hsts_header_without_subdomains(self): """Test HSTS header without subdomains.""" config = SecurityHeadersConfig(hsts_include_subdomains=False) hsts_header = config.hsts_header_value assert "includeSubDomains" not in hsts_header def test_csp_header_generation(self): """Test CSP header value generation.""" config = SecurityHeadersConfig() csp_header = config.csp_header_value # Check required directives assert "default-src 'self'" in csp_header assert "script-src 'self' 'unsafe-inline'" in csp_header assert "style-src 'self' 'unsafe-inline'" in csp_header assert "object-src 'none'" in csp_header assert "connect-src 'self'" in csp_header assert "frame-src 'none'" in csp_header assert "base-uri 'self'" in csp_header assert "form-action 'self'" in csp_header def test_csp_custom_directives(self): """Test CSP with custom directives.""" config = SecurityHeadersConfig( csp_script_src=["'self'", "https://trusted.com"], csp_connect_src=["'self'", "https://api.trusted.com"], ) csp_header = config.csp_header_value assert "script-src 'self' https://trusted.com" in csp_header assert "connect-src 'self' https://api.trusted.com" in csp_header def test_permissions_policy_default(self): """Test permissions policy default configuration.""" config = SecurityHeadersConfig() permissions = config.permissions_policy assert "geolocation=()" in permissions assert "microphone=()" in permissions assert "camera=()" in permissions assert "usb=()" in permissions assert "magnetometer=()" in permissions class TestSecurityHeadersMiddleware: """Test security headers middleware implementation.""" def test_middleware_adds_headers(self): """Test that middleware adds security headers to responses.""" app = FastAPI() # Create mock security config mock_config = MagicMock() mock_config.get_security_headers.return_value = { "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "X-XSS-Protection": "1; mode=block", "Content-Security-Policy": "default-src 'self'", } app.add_middleware(SecurityHeadersMiddleware, security_config=mock_config) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") assert response.headers["X-Content-Type-Options"] == "nosniff" assert response.headers["X-Frame-Options"] == "DENY" assert response.headers["X-XSS-Protection"] == "1; mode=block" assert response.headers["Content-Security-Policy"] == "default-src 'self'" def test_middleware_uses_default_config(self): """Test that middleware uses default security config when none provided.""" app = FastAPI() with patch( "maverick_mcp.config.security_utils.get_security_config" ) as mock_get_config: mock_config = MagicMock() mock_config.get_security_headers.return_value = {"X-Frame-Options": "DENY"} mock_get_config.return_value = mock_config app.add_middleware(SecurityHeadersMiddleware) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") mock_get_config.assert_called_once() assert response.headers["X-Frame-Options"] == "DENY" def test_api_middleware_integration(self): """Test API security headers middleware integration.""" app = FastAPI() app.add_middleware(APISecurityHeadersMiddleware) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") # Should have basic security headers assert "X-Content-Type-Options" in response.headers assert "X-Frame-Options" in response.headers class TestEnvironmentSpecificHeaders: """Test environment-specific security headers.""" def test_hsts_in_production(self): """Test HSTS header is included in production.""" with patch.dict(os.environ, {"ENVIRONMENT": "production"}, clear=False): config = SecurityConfig() headers = config.get_security_headers() assert "Strict-Transport-Security" in headers assert "max-age=" in headers["Strict-Transport-Security"] def test_hsts_in_development(self): """Test HSTS header is not included in development.""" with patch.dict(os.environ, {"ENVIRONMENT": "development"}, clear=False): config = SecurityConfig(force_https=False) headers = config.get_security_headers() assert "Strict-Transport-Security" not in headers def test_hsts_with_force_https(self): """Test HSTS header is included when HTTPS is forced.""" with patch.dict(os.environ, {"ENVIRONMENT": "development"}, clear=False): config = SecurityConfig(force_https=True) headers = config.get_security_headers() assert "Strict-Transport-Security" in headers def test_production_security_validation(self): """Test production security validation.""" with patch.dict(os.environ, {"ENVIRONMENT": "production"}, clear=False): with patch( "maverick_mcp.config.security._get_cors_origins" ) as mock_origins: mock_origins.return_value = ["https://app.maverick-mcp.com"] with patch("logging.getLogger") as mock_logger: mock_logger_instance = MagicMock() mock_logger.return_value = mock_logger_instance # Test with HTTPS not forced (should warn) SecurityConfig(force_https=False) # Should log warning about HTTPS mock_logger_instance.warning.assert_called() def test_development_security_permissive(self): """Test development security is more permissive.""" with patch.dict(os.environ, {"ENVIRONMENT": "development"}, clear=False): config = SecurityConfig() assert config.is_development() is True assert config.is_production() is False class TestCSPConfiguration: """Test Content Security Policy configuration.""" def test_csp_avoids_checkout_domains(self): """Test CSP excludes third-party checkout provider domains.""" config = SecurityHeadersConfig() assert config.csp_script_src == ["'self'", "'unsafe-inline'"] assert config.csp_connect_src == ["'self'"] assert config.csp_frame_src == ["'none'"] def test_csp_blocks_inline_scripts_by_default(self): """Test CSP configuration for inline scripts.""" config = SecurityHeadersConfig() csp = config.csp_header_value # Note: Current config allows 'unsafe-inline' for compatibility # In a more secure setup, this should use nonces or hashes assert "'unsafe-inline'" in csp def test_csp_blocks_object_embedding(self): """Test CSP blocks object embedding.""" config = SecurityHeadersConfig() csp = config.csp_header_value assert "object-src 'none'" in csp def test_csp_restricts_base_uri(self): """Test CSP restricts base URI.""" config = SecurityHeadersConfig() csp = config.csp_header_value assert "base-uri 'self'" in csp def test_csp_restricts_form_action(self): """Test CSP restricts form actions.""" config = SecurityHeadersConfig() csp = config.csp_header_value assert "form-action 'self'" in csp def test_csp_image_sources(self): """Test CSP allows necessary image sources.""" config = SecurityHeadersConfig() csp = config.csp_header_value assert "img-src 'self' data: https:" in csp def test_csp_custom_configuration(self): """Test CSP with custom configuration.""" custom_config = SecurityHeadersConfig( csp_default_src=["'self'", "https://trusted.com"], csp_script_src=["'self'"], csp_style_src=["'self'"], # Remove unsafe-inline from styles too csp_object_src=["'none'"], ) csp = custom_config.csp_header_value assert "default-src 'self' https://trusted.com" in csp assert "script-src 'self'" in csp # Since we removed unsafe-inline from style-src, it shouldn't be in CSP assert "style-src 'self'" in csp assert "'unsafe-inline'" not in csp class TestXFrameOptionsConfiguration: """Test X-Frame-Options configuration.""" def test_frame_options_deny_default(self): """Test X-Frame-Options defaults to DENY.""" SecurityHeadersConfig() headers = SecurityConfig().get_security_headers() assert headers["X-Frame-Options"] == "DENY" def test_frame_options_sameorigin(self): """Test X-Frame-Options can be set to SAMEORIGIN.""" config = SecurityHeadersConfig(x_frame_options="SAMEORIGIN") security_config = SecurityConfig(headers=config) headers = security_config.get_security_headers() assert headers["X-Frame-Options"] == "SAMEORIGIN" def test_frame_options_allow_from(self): """Test X-Frame-Options with ALLOW-FROM directive.""" config = SecurityHeadersConfig(x_frame_options="ALLOW-FROM https://trusted.com") security_config = SecurityConfig(headers=config) headers = security_config.get_security_headers() assert headers["X-Frame-Options"] == "ALLOW-FROM https://trusted.com" class TestReferrerPolicyConfiguration: """Test Referrer-Policy configuration.""" def test_referrer_policy_default(self): """Test Referrer-Policy default value.""" SecurityHeadersConfig() headers = SecurityConfig().get_security_headers() assert headers["Referrer-Policy"] == "strict-origin-when-cross-origin" def test_referrer_policy_custom(self): """Test custom Referrer-Policy.""" config = SecurityHeadersConfig(referrer_policy="no-referrer") security_config = SecurityConfig(headers=config) headers = security_config.get_security_headers() assert headers["Referrer-Policy"] == "no-referrer" class TestPermissionsPolicyConfiguration: """Test Permissions-Policy configuration.""" def test_permissions_policy_blocks_dangerous_features(self): """Test Permissions-Policy blocks dangerous browser features.""" SecurityHeadersConfig() headers = SecurityConfig().get_security_headers() permissions = headers["Permissions-Policy"] assert "geolocation=()" in permissions assert "microphone=()" in permissions assert "camera=()" in permissions assert "usb=()" in permissions def test_permissions_policy_custom(self): """Test custom Permissions-Policy configuration.""" custom_policy = "geolocation=(self), camera=(), microphone=()" config = SecurityHeadersConfig(permissions_policy=custom_policy) security_config = SecurityConfig(headers=config) headers = security_config.get_security_headers() assert headers["Permissions-Policy"] == custom_policy class TestSecurityHeadersIntegration: """Test security headers integration with application.""" def test_all_headers_applied(self): """Test that all security headers are applied to responses.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") # Check all expected headers are present expected_headers = [ "X-Content-Type-Options", "X-Frame-Options", "X-XSS-Protection", "Referrer-Policy", "Permissions-Policy", "Content-Security-Policy", ] for header in expected_headers: assert header in response.headers def test_headers_on_error_responses(self): """Test security headers are included on error responses.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/error") async def error_endpoint(): from fastapi import HTTPException raise HTTPException(status_code=500, detail="Test error") client = TestClient(app) response = client.get("/error") # Even on errors, security headers should be present assert response.status_code == 500 assert "X-Frame-Options" in response.headers assert "X-Content-Type-Options" in response.headers def test_headers_on_different_methods(self): """Test security headers on different HTTP methods.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def get_endpoint(): return {"method": "GET"} @app.post("/test") async def post_endpoint(): return {"method": "POST"} @app.put("/test") async def put_endpoint(): return {"method": "PUT"} client = TestClient(app) methods = [(client.get, "/test"), (client.post, "/test"), (client.put, "/test")] for method_func, path in methods: response = method_func(path) assert "X-Frame-Options" in response.headers assert "Content-Security-Policy" in response.headers def test_headers_override_existing(self): """Test security headers override any existing headers.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): from fastapi import Response response = Response(content='{"message": "test"}') response.headers["X-Frame-Options"] = "ALLOWALL" # Insecure value return response client = TestClient(app) response = client.get("/test") # Security middleware should override the insecure value assert response.headers["X-Frame-Options"] == "DENY" class TestSecurityHeadersValidation: """Test security headers validation and best practices.""" def test_no_server_header_disclosure(self): """Test that server information is not disclosed.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") # Should not disclose server information server_header = response.headers.get("Server", "") assert "uvicorn" not in server_header.lower() def test_no_powered_by_header(self): """Test that X-Powered-By header is not present.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") assert "X-Powered-By" not in response.headers def test_content_type_nosniff(self): """Test X-Content-Type-Options prevents MIME sniffing.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") assert response.headers["X-Content-Type-Options"] == "nosniff" def test_xss_protection_enabled(self): """Test X-XSS-Protection is properly configured.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) response = client.get("/test") xss_protection = response.headers["X-XSS-Protection"] assert "1" in xss_protection assert "mode=block" in xss_protection class TestSecurityHeadersPerformance: """Test security headers don't impact performance significantly.""" def test_headers_middleware_performance(self): """Test security headers middleware performance.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) # Make multiple requests to test performance import time start_time = time.time() for _ in range(100): response = client.get("/test") assert response.status_code == 200 end_time = time.time() total_time = end_time - start_time # Should complete 100 requests quickly (less than 5 seconds) assert total_time < 5.0 def test_headers_memory_usage(self): """Test security headers don't cause memory leaks.""" app = FastAPI() apply_security_headers_to_fastapi(app) @app.get("/test") async def test_endpoint(): return {"message": "test"} client = TestClient(app) # Make many requests to check for memory leaks for _ in range(1000): response = client.get("/test") assert "X-Frame-Options" in response.headers # If we reach here without memory issues, test passes if __name__ == "__main__": pytest.main([__file__, "-v"])

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wshobson/maverick-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server