Skip to main content
Glama
test_credential_security.py6.22 kB
""" Security Tests - Credential Leakage Prevention Tests to verify that credentials are never exposed in: - Log messages - Tool responses - Error messages - Configuration output """ import pytest import logging import json import os from pathlib import Path from unittest.mock import patch, Mock, AsyncMock from mcp.server.fastmcp import Context from src.opnsense_mcp.core.config_loader import ConfigLoader from src.opnsense_mcp.core.models import OPNsenseConfig from src.opnsense_mcp.domains.configuration import configure_opnsense_connection @pytest.fixture def secure_config_setup(tmp_path, monkeypatch): """Setup config with sensitive credentials for security testing.""" config_dir = tmp_path / ".opnsense-mcp" config_dir.mkdir() config_file = config_dir / "config.json" monkeypatch.setattr(ConfigLoader, "DEFAULT_CONFIG_DIR", config_dir) monkeypatch.setattr(ConfigLoader, "DEFAULT_CONFIG_FILE", config_file) # Create test profile with distinctive credentials to detect leaks config_data = { "default": { "url": "https://192.168.1.1", "api_key": "SENSITIVE_API_KEY_12345", "api_secret": "SENSITIVE_SECRET_67890", "verify_ssl": True } } with open(config_file, 'w') as f: json.dump(config_data, f) os.chmod(config_file, 0o600) return config_file class TestCredentialLeakagePrevention: """Test that credentials are never leaked in various contexts.""" @pytest.mark.asyncio async def test_tool_response_no_credentials(self, secure_config_setup): """Test that tool response never contains credentials.""" mock_ctx = Mock(spec=Context) mock_ctx.info = AsyncMock() mock_ctx.error = AsyncMock() with patch('src.opnsense_mcp.domains.configuration.server_state') as mock_state: mock_state.initialize = AsyncMock() result = await configure_opnsense_connection(mock_ctx, profile="default") # Verify NO credentials in response assert "SENSITIVE_API_KEY_12345" not in result assert "SENSITIVE_SECRET_67890" not in result @pytest.mark.asyncio async def test_error_messages_no_credentials(self, secure_config_setup): """Test that error messages never contain credentials.""" mock_ctx = Mock(spec=Context) mock_ctx.info = AsyncMock() mock_ctx.error = AsyncMock() with patch('src.opnsense_mcp.domains.configuration.server_state') as mock_state: # Simulate authentication error mock_state.initialize = AsyncMock(side_effect=Exception("Auth failed")) result = await configure_opnsense_connection(mock_ctx, profile="default") # Verify NO credentials in error message assert "SENSITIVE_API_KEY_12345" not in result assert "SENSITIVE_SECRET_67890" not in result def test_config_model_repr_hides_secret(self, secure_config_setup): """Test that OPNsenseConfig repr() doesn't expose API secret.""" config = OPNsenseConfig( url="https://192.168.1.1", api_key="SENSITIVE_API_KEY_12345", api_secret="SENSITIVE_SECRET_67890", verify_ssl=True ) repr_output = repr(config) # API secret should be hidden (repr=False in model) assert "SENSITIVE_SECRET_67890" not in repr_output def test_profile_info_partial_key_only(self, secure_config_setup): """Test that profile info only shows partial API key.""" info = ConfigLoader.get_profile_info("default") # Should have preview assert "api_key_preview" in info # Preview should NOT contain full key assert "SENSITIVE_API_KEY_12345" not in str(info) # Should only show first 4 + last 4 chars assert len(info["api_key_preview"].replace("...", "")) == 8 def test_config_file_permissions(self, secure_config_setup): """Test that config file has secure permissions (0600).""" stat_info = os.stat(secure_config_setup) perms = stat_info.st_mode & 0o777 # Should be 0600 (owner read/write only) assert perms == 0o600 @pytest.mark.asyncio async def test_logging_no_credentials(self, secure_config_setup, caplog): """Test that log messages never contain credentials.""" caplog.set_level(logging.DEBUG) mock_ctx = Mock(spec=Context) mock_ctx.info = AsyncMock() mock_ctx.error = AsyncMock() with patch('src.opnsense_mcp.domains.configuration.server_state') as mock_state: mock_state.initialize = AsyncMock() await configure_opnsense_connection(mock_ctx, profile="default") # Check all log messages log_output = caplog.text # Verify NO credentials in logs assert "SENSITIVE_API_KEY_12345" not in log_output assert "SENSITIVE_SECRET_67890" not in log_output def test_config_file_json_readable_but_protected(self, secure_config_setup): """Test that config file is readable JSON but has secure permissions.""" # Should be able to read as JSON with open(secure_config_setup, 'r') as f: config_data = json.load(f) assert "default" in config_data assert config_data["default"]["api_key"] == "SENSITIVE_API_KEY_12345" # But permissions should be restrictive stat_info = os.stat(secure_config_setup) perms = stat_info.st_mode & 0o777 assert perms == 0o600 @pytest.mark.asyncio async def test_tool_signature_no_credential_parameters(self): """Test that tool signature doesn't accept credentials as parameters.""" import inspect sig = inspect.signature(configure_opnsense_connection) # Should only have ctx and profile parameters params = list(sig.parameters.keys()) assert "ctx" in params assert "profile" in params assert "url" not in params # OLD insecure parameter removed assert "api_key" not in params # OLD insecure parameter removed assert "api_secret" not in params # OLD insecure parameter removed

Latest Blog Posts

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/floriangrousset/opnsense-mcp-server'

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