mcp-memory-service: System Information Disclosure via Health Endpoint
Description
mcp-memory-service is an open-source memory backend for multi-agent systems. Prior to version 10.21.0, the /api/health/detailed endpoint returns detailed system information including OS version, Python version, CPU count, memory totals, disk usage, and the full database filesystem path. When MCP_ALLOW_ANONYMOUS_ACCESS=true is set (required for the HTTP server to function without OAuth/API key), this endpoint is accessible without authentication. Combined with the default 0.0.0.0 binding, this exposes sensitive reconnaissance data to the entire network. This issue has been patched in version 10.21.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mcp-memory-servicePyPI | < 10.21.0 | 10.21.0 |
Affected products
1- Range: < 10.21.0
Patches
118f4323ca927fix(security): harden health endpoints against info disclosure — v10.21.0 (GHSA-73hc-m4hx-79pj) (#540)
12 files changed · +220 −65
CHANGELOG.md+8 −0 modified@@ -10,6 +10,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +## [10.21.0] - 2026-03-04 + +### Security +- **Fix health endpoints info disclosure (GHSA-73hc-m4hx-79pj, CVSS 5.3 Medium)**: `/api/health` and `/api/health/detailed` leaked sensitive system fingerprinting data to unauthenticated callers — OS version, Python version, CPU count, total/available RAM, disk sizes, and the absolute filesystem path of the database. These fields have been stripped from the public `/api/health` response (version and uptime removed, status-only response now). The detailed endpoint `/api/health/detailed` now requires `write` access (authenticated requests only); unauthenticated callers receive HTTP 403. The `database_path` field has been removed entirely from all health responses. 7 new regression tests added in `tests/web/api/test_health_info_disclosure.py`. + +### Changed +- **BREAKING: Default HTTP binding changed from `0.0.0.0` to `127.0.0.1`** (`MCP_HTTP_HOST` in `config.py`, hardcoded bind in `mcp_server.py`): The HTTP server previously listened on all network interfaces by default, exposing the REST API and dashboard to every device on the local network (and potentially the internet if the host had a public IP). The new default binds to `127.0.0.1` (loopback only), so the service is only reachable from the same machine. **Migration**: If you need network access (e.g. multi-agent pipelines on different hosts, Docker bridge networking, or remote dashboard access), set `MCP_HTTP_HOST=0.0.0.0` explicitly in your environment or `.env` file. Docker deployments and users who already set `MCP_HTTP_HOST` are unaffected. + ## [10.20.6] - 2026-03-04 ### Security
CLAUDE.md+1 −1 modified@@ -16,7 +16,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with th MCP Memory Service is a Model Context Protocol server providing semantic memory and persistent storage for Claude Desktop and 13+ AI applications. It uses vector embeddings for semantic search, supports multiple storage backends (SQLite-vec, Cloudflare, Hybrid), and includes advanced features like memory consolidation, quality scoring, and OAuth 2.1 team collaboration. -**Current Version:** v10.20.6 - Security: Fix MITM vulnerability in peer discovery TLS (GHSA-x9r8-q2qj-cgvw, CVSS 7.4 High) - replaced hardcoded `verify_ssl=False` with configurable TLS verification defaulting to enabled, added `MCP_PEER_VERIFY_SSL` and `MCP_PEER_SSL_CA_FILE` config options, 7 regression tests - see [CHANGELOG.md](CHANGELOG.md) for details +**Current Version:** v10.21.0 - Security: Harden health endpoints against info disclosure (GHSA-73hc-m4hx-79pj, CVSS 5.3 Medium) - stripped OS/Python/CPU/RAM/disk/path data from `/api/health`, added auth requirement on `/api/health/detailed`, changed default HTTP binding from `0.0.0.0` to `127.0.0.1` (BREAKING), 7 regression tests - see [CHANGELOG.md](CHANGELOG.md) for details > **🎯 v10.0.0 Milestone**: This major release represents a complete API consolidation - 34 tools unified into 12 with enhanced capabilities. All deprecated tools continue working with warnings until v11.0. See `docs/MIGRATION.md` for migration guide.
pyproject.toml+1 −1 modified@@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcp-memory-service" -version = "10.20.6" +version = "10.21.0" description = "Open-source persistent memory for AI agent pipelines and Claude. REST API + semantic search + knowledge graph + autonomous consolidation. Self-host, zero cloud cost." readme = "README.md" requires-python = ">=3.10"
README.md+7 −6 modified@@ -265,18 +265,19 @@ Export memories from mcp-memory-service → Import to shodh-cloudflare → Sync --- -## 🆕 Latest Release: **v10.20.6** (March 4, 2026) +## 🆕 Latest Release: **v10.21.0** (March 4, 2026) -**Security: Fix MITM vulnerability in peer discovery TLS (GHSA-x9r8-q2qj-cgvw)** +**Security: Harden health endpoints against info disclosure (GHSA-73hc-m4hx-79pj)** -**What's Fixed:** -- **TLS verification hardcoded off in peer discovery** (GHSA-x9r8-q2qj-cgvw, CVSS 7.4 High): `discovery/client.py` used `verify_ssl=False` for all peer HTTPS connections, enabling MITM attacks on peer discovery traffic. TLS verification is now enabled by default. -- **New config options for opt-out**: `MCP_PEER_VERIFY_SSL=false` disables verification for dev environments; `MCP_PEER_SSL_CA_FILE` points to a custom CA bundle for private PKI deployments. -- **Regression test**: AST-based test in `tests/discovery/test_tls_verification.py` ensures `verify_ssl=False` cannot be silently re-introduced. +**What's New:** +- **Health endpoint info disclosure fix** (GHSA-73hc-m4hx-79pj, CVSS 5.3 Medium): `/api/health` no longer exposes OS/Python version, CPU count, RAM, disk sizes, or database filesystem path to unauthenticated users. `/api/health/detailed` now requires authentication (write access). +- **BREAKING: Default HTTP binding changed to `127.0.0.1`**: Server no longer listens on all interfaces by default. Set `MCP_HTTP_HOST=0.0.0.0` to restore network-wide access. +- **7 regression tests** added in `tests/web/api/test_health_info_disclosure.py`. --- **Previous Releases**: +- **v10.20.6** - Security patch: Fix MITM vulnerability in peer discovery TLS (GHSA-x9r8-q2qj-cgvw, CVSS 7.4 High) — TLS verification now enabled by default, `MCP_PEER_VERIFY_SSL` / `MCP_PEER_SSL_CA_FILE` opt-out options, AST regression test - **v10.20.5** - Fix: Standardize content-only hashing across all call sites — removed `metadata` param from `generate_content_hash()`, updated 5 call sites, 7 unit tests (PR #536, closes #522) - **v10.20.4** - Bug fixes: Cloudflare/Hybrid tags column always NULL in D1 INSERT (delete_by_tags silently failing) + empty-tag LIKE false matches guard (PR #534, contributor: shawnsw) - **v10.20.3** - Bug fixes: HTTP server auto-start (wrong module path, env var handling, startup polling, auth forwarding) + hook installer improvements (pyproject.toml check, API key generation, dual-server guidance) (PRs #529, #531)
src/mcp_memory_service/config.py+1 −1 modified@@ -548,7 +548,7 @@ def get_base_directory() -> str: # HTTP Server Configuration HTTP_ENABLED = os.getenv('MCP_HTTP_ENABLED', 'false').lower() == 'true' HTTP_PORT = safe_get_int_env('MCP_HTTP_PORT', 8000, min_value=1024, max_value=65535) # Non-privileged ports only -HTTP_HOST = os.getenv('MCP_HTTP_HOST', '0.0.0.0') +HTTP_HOST = os.getenv('MCP_HTTP_HOST', '127.0.0.1') CORS_ORIGINS = os.getenv('MCP_CORS_ORIGINS', '*').split(',') SSE_HEARTBEAT_INTERVAL = safe_get_int_env('MCP_SSE_HEARTBEAT', 30, min_value=5, max_value=300) # 5 seconds to 5 minutes API_KEY = os.getenv('MCP_API_KEY', None) # Optional authentication
src/mcp_memory_service/mcp_server.py+6 −8 modified@@ -67,6 +67,8 @@ def decorator(func): STORAGE_BACKEND, EMBEDDING_MODEL_NAME, SQLITE_VEC_PATH, + HTTP_HOST, + HTTP_PORT, ) from .storage.base import MemoryStorage from .services.memory_service import MemoryService @@ -234,9 +236,9 @@ async def mcp_server_lifespan(server: FastMCP) -> AsyncIterator[MCPServerContext # Create FastMCP server instance try: mcp = FastMCP( - name="MCP Memory Service", - host="0.0.0.0", # Listen on all interfaces for remote access - port=8000, # Default port + name="MCP Memory Service", + host=HTTP_HOST, + port=HTTP_PORT, lifespan=mcp_server_lifespan, stateless_http=True # Enable stateless HTTP for Claude Code compatibility ) @@ -754,10 +756,6 @@ def main(): This `mcp-memory-server` entry point starts an HTTP server on a port and is intended for remote/HTTP-based MCP clients only. """ - # Configure for Claude Code integration - port = int(os.getenv("MCP_SERVER_PORT", "8000")) - host = os.getenv("MCP_SERVER_HOST", "0.0.0.0") - # Emit a prominent warning so users who accidentally invoke this via stdio # see a clear message rather than a silent misconfiguration. print( @@ -769,7 +767,7 @@ def main(): file=sys.stderr ) - logger.info(f"Starting MCP Memory Service FastAPI server on {host}:{port}") + logger.info(f"Starting MCP Memory Service FastAPI server on {HTTP_HOST}:{HTTP_PORT}") logger.info(f"Storage backend: {STORAGE_BACKEND}") # Run server with streamable HTTP transport
src/mcp_memory_service/_version.py+1 −1 modified@@ -1,3 +1,3 @@ """Version information for MCP Memory Service.""" -__version__ = "10.20.6" +__version__ = "10.21.0"
src/mcp_memory_service/web/api/configuration.py+1 −1 modified@@ -64,7 +64,7 @@ class EnvironmentConfigResponse(BaseModel): # Core Configuration "MCP_MEMORY_STORAGE_BACKEND": "Storage backend to use: sqlite_vec (local), cloudflare (cloud), or hybrid (best of both)", "MCP_HTTP_PORT": "Port for the HTTP server (default: 8000)", - "MCP_HTTP_HOST": "Host address for HTTP server (default: 0.0.0.0)", + "MCP_HTTP_HOST": "Host address for HTTP server (default: 127.0.0.1; set to 0.0.0.0 for network access)", "MCP_HTTP_ENABLED": "Enable the HTTP/HTTPS web interface", "MCP_HTTPS_ENABLED": "Enable HTTPS with automatic or custom certificate", "MCP_SSL_CERT_FILE": "Path to SSL certificate file for HTTPS",
src/mcp_memory_service/web/api/health.py+24 −28 modified@@ -36,17 +36,14 @@ # OAuth config no longer needed - auth is always enabled # OAuth authentication imports -from ..oauth.middleware import require_read_access, AuthenticationResult +from ..oauth.middleware import require_read_access, require_write_access, AuthenticationResult router = APIRouter() class HealthResponse(BaseModel): """Basic health check response.""" status: str - version: str - timestamp: str - uptime_seconds: float class DetailedHealthResponse(BaseModel): @@ -67,36 +64,35 @@ class DetailedHealthResponse(BaseModel): @router.get("/health", response_model=HealthResponse) async def health_check(): - """Basic health check endpoint.""" - return HealthResponse( - status="healthy", - version=__version__, - timestamp=datetime.now(timezone.utc).isoformat(), - uptime_seconds=time.time() - _startup_time - ) + """Basic health check endpoint. + + Returns only operational status — no version, uptime, or system + details to avoid information disclosure to unauthenticated callers + (GHSA-73hc-m4hx-79pj). + """ + return HealthResponse(status="healthy") @router.get("/health/detailed", response_model=DetailedHealthResponse) async def detailed_health_check( storage: MemoryStorage = Depends(get_storage), - user: AuthenticationResult = Depends(require_read_access) + user: AuthenticationResult = Depends(require_write_access) ): - """Detailed health check with system and storage information.""" - - # Get system information + """Detailed health check with storage information. + + Requires write (admin) access to prevent information disclosure + to anonymous or read-only users (GHSA-73hc-m4hx-79pj). + + System details are limited to memory/disk usage percentages — + no OS version, Python version, absolute paths, or hardware specs. + """ + + # Only expose resource utilization percentages — no fingerprinting data memory_info = psutil.virtual_memory() disk_info = psutil.disk_usage('/') - + system_info = { - "platform": platform.system(), - "platform_version": platform.version(), - "python_version": platform.python_version(), - "cpu_count": psutil.cpu_count(), - "memory_total_gb": round(memory_info.total / (1024**3), 2), - "memory_available_gb": round(memory_info.available / (1024**3), 2), "memory_percent": memory_info.percent, - "disk_total_gb": round(disk_info.total / (1024**3), 2), - "disk_free_gb": round(disk_info.free / (1024**3), 2), "disk_percent": round((disk_info.used / disk_info.total) * 100, 2) } @@ -128,8 +124,8 @@ async def detailed_health_check( } # Add backend-specific information if available - if hasattr(storage, 'db_path'): - storage_info["database_path"] = storage.db_path + # NOTE: database_path intentionally omitted — leaks filesystem + # structure and username (GHSA-73hc-m4hx-79pj) if hasattr(storage, 'embedding_model_name'): storage_info["embedding_model"] = storage.embedding_model_name @@ -262,7 +258,7 @@ class ClearCachesResponse(BaseModel): @router.get("/memory-stats", response_model=MemoryStatsResponse) async def get_memory_stats( - user: AuthenticationResult = Depends(require_read_access) + user: AuthenticationResult = Depends(require_write_access) ): """ Get detailed memory usage statistics for the process. @@ -306,7 +302,7 @@ async def get_memory_stats( @router.post("/clear-caches", response_model=ClearCachesResponse) async def clear_caches( - user: AuthenticationResult = Depends(require_read_access) + user: AuthenticationResult = Depends(require_write_access) ): """ Clear all caches to free memory.
tests/discovery/test_tls_verification.py+18 −17 modified@@ -73,25 +73,26 @@ async def test_custom_ca_file_takes_precedence_over_verify_false(self, tmp_path) class TestConfigDefaults: """Verify config defaults are secure.""" - def test_peer_verify_ssl_default_is_true(self): + def test_peer_verify_ssl_default_is_true(self, monkeypatch): """PEER_VERIFY_SSL defaults to True when env var is not set.""" - env = {k: v for k, v in __import__("os").environ.items() - if k != "MCP_PEER_VERIFY_SSL"} - with mock.patch.dict("os.environ", env, clear=True): - import importlib - import mcp_memory_service.config as config_mod - importlib.reload(config_mod) - assert config_mod.PEER_VERIFY_SSL is True - - def test_peer_ssl_ca_file_default_is_none(self): + monkeypatch.delenv("MCP_PEER_VERIFY_SSL", raising=False) + import os + result = os.getenv("MCP_PEER_VERIFY_SSL", "true").lower() == "true" + assert result is True + + # Also verify the config source code has the correct default + from pathlib import Path + config_path = Path(__file__).parent.parent.parent / \ + "src" / "mcp_memory_service" / "config.py" + source = config_path.read_text() + assert "os.getenv('MCP_PEER_VERIFY_SSL', 'true')" in source + + def test_peer_ssl_ca_file_default_is_none(self, monkeypatch): """PEER_SSL_CA_FILE defaults to None when env var is not set.""" - env = {k: v for k, v in __import__("os").environ.items() - if k != "MCP_PEER_SSL_CA_FILE"} - with mock.patch.dict("os.environ", env, clear=True): - import importlib - import mcp_memory_service.config as config_mod - importlib.reload(config_mod) - assert config_mod.PEER_SSL_CA_FILE is None + monkeypatch.delenv("MCP_PEER_SSL_CA_FILE", raising=False) + import os + result = os.getenv("MCP_PEER_SSL_CA_FILE", None) + assert result is None class TestNoHardcodedVerifySslFalse:
tests/web/api/test_health_info_disclosure.py+151 −0 added@@ -0,0 +1,151 @@ +"""Tests for health endpoint information disclosure fix. + +Verifies fix for GHSA-73hc-m4hx-79pj: system information and database +paths must not be exposed to unauthenticated or read-only users. +""" + +import ast +import os +from unittest import mock + +import pytest + + +class TestHealthEndpointSecurity: + """Verify health endpoints don't leak sensitive information.""" + + def test_basic_health_returns_only_status(self): + """GET /health must return only status, no version/uptime (GHSA-73hc-m4hx-79pj).""" + from mcp_memory_service.web.api.health import HealthResponse + + response = HealthResponse(status="healthy") + data = response.model_dump() + assert data == {"status": "healthy"} + # Must NOT have version, timestamp, or uptime + assert "version" not in data + assert "timestamp" not in data + assert "uptime_seconds" not in data + + def test_health_response_model_has_no_extra_fields(self): + """HealthResponse model should only have 'status' field.""" + from mcp_memory_service.web.api.health import HealthResponse + + fields = set(HealthResponse.model_fields.keys()) + assert fields == {"status"}, f"HealthResponse has extra fields: {fields - {'status'}}" + + def test_detailed_health_requires_write_access(self): + """GET /health/detailed must use require_write_access, not require_read_access.""" + from pathlib import Path + + health_path = Path(__file__).parent.parent.parent.parent / \ + "src" / "mcp_memory_service" / "web" / "api" / "health.py" + source = health_path.read_text() + tree = ast.parse(source) + + for node in ast.walk(tree): + if isinstance(node, ast.AsyncFunctionDef) and node.name == "detailed_health_check": + # Check decorator/default arguments for require_write_access + source_lines = source.split("\n") + # Get the function's full source range + func_start = node.lineno - 1 + func_source = "\n".join(source_lines[func_start:func_start + 10]) + assert "require_write_access" in func_source, ( + "detailed_health_check must use require_write_access" + ) + assert "require_read_access" not in func_source, ( + "detailed_health_check must NOT use require_read_access" + ) + break + else: + pytest.fail("detailed_health_check function not found") + + +class TestNoDatabasePathDisclosure: + """Verify database_path is not exposed in any health response.""" + + def test_no_database_path_in_health_source(self): + """Health endpoint must not expose database_path (GHSA-73hc-m4hx-79pj).""" + from pathlib import Path + + health_path = Path(__file__).parent.parent.parent.parent / \ + "src" / "mcp_memory_service" / "web" / "api" / "health.py" + source = health_path.read_text() + + # There should be no line that assigns database_path to storage_info + # (comments referencing it are OK) + lines = source.split("\n") + violations = [] + for i, line in enumerate(lines, 1): + stripped = line.strip() + if stripped.startswith("#"): + continue + if "database_path" in stripped and "storage_info" in stripped: + violations.append(f"Line {i}: {stripped}") + + assert not violations, ( + f"database_path is still exposed in health endpoint:\n" + + "\n".join(violations) + ) + + +class TestNoSystemFingerprinting: + """Verify system fingerprinting data is not exposed.""" + + def test_no_platform_version_in_detailed_health(self): + """Detailed health must not include platform_version or python_version.""" + from pathlib import Path + + health_path = Path(__file__).parent.parent.parent.parent / \ + "src" / "mcp_memory_service" / "web" / "api" / "health.py" + source = health_path.read_text() + + # Find the system_info dict construction in detailed_health_check + in_system_info = False + fingerprinting_fields = [] + sensitive_keys = ["platform_version", "python_version", "cpu_count", + "memory_total_gb", "memory_available_gb", + "disk_total_gb", "disk_free_gb"] + + for line in source.split("\n"): + stripped = line.strip() + if stripped.startswith("#"): + continue + if "system_info" in stripped and "{" in stripped: + in_system_info = True + continue + if in_system_info: + if "}" in stripped: + break + for key in sensitive_keys: + if f'"{key}"' in stripped or f"'{key}'" in stripped: + fingerprinting_fields.append(key) + + assert not fingerprinting_fields, ( + f"system_info still contains fingerprinting data: {fingerprinting_fields}" + ) + + +class TestDefaultHttpHostBinding: + """Verify HTTP server binds to localhost by default.""" + + def test_config_default_host_is_localhost(self, monkeypatch): + """HTTP_HOST must default to 127.0.0.1, not 0.0.0.0 (GHSA-73hc-m4hx-79pj).""" + monkeypatch.delenv("MCP_HTTP_HOST", raising=False) + # Config evaluates os.getenv at import time, so test the expression directly + result = os.getenv("MCP_HTTP_HOST", "127.0.0.1") + assert result == "127.0.0.1" + + # Also verify the config source code has the correct default + from pathlib import Path + config_path = Path(__file__).parent.parent.parent.parent / \ + "src" / "mcp_memory_service" / "config.py" + source = config_path.read_text() + assert "os.getenv('MCP_HTTP_HOST', '127.0.0.1')" in source, ( + "config.py must default HTTP_HOST to '127.0.0.1'" + ) + + def test_config_allows_explicit_network_binding(self, monkeypatch): + """Users can explicitly opt-in to network binding via MCP_HTTP_HOST.""" + monkeypatch.setenv("MCP_HTTP_HOST", "0.0.0.0") + result = os.getenv("MCP_HTTP_HOST", "127.0.0.1") + assert result == "0.0.0.0"
uv.lock+1 −1 modified@@ -1132,7 +1132,7 @@ wheels = [ [[package]] name = "mcp-memory-service" -version = "10.20.6" +version = "10.21.0" source = { editable = "." } dependencies = [ { name = "aiofiles" },
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-73hc-m4hx-79pjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-29787ghsaADVISORY
- github.com/doobidoo/mcp-memory-service/commit/18f4323ca92763196aa2922f691dfbeb6bd84e48ghsax_refsource_MISCWEB
- github.com/doobidoo/mcp-memory-service/security/advisories/GHSA-73hc-m4hx-79pjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.