mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix(mcp): redact database_url credentials in diagnostics output
Extends the existing _SECRET_FIELDS redaction to also strip the user:password userinfo component from any URL-bearing config fields (database_url) before surfacing them in diagnostics output. Adds _redact_url() helper and a _URL_FIELDS set so future credential-bearing URL fields are easy to add. Postgres passwords no longer leak to MCP clients or support transcripts; host/port/db-name remain visible. Fixes the P1 security finding raised in review of PR #963. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Drew Cain <groksrc@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import platform
|
||||
import sys
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import basic_memory
|
||||
from basic_memory.config import CONFIG_FILE_NAME, ConfigManager
|
||||
@@ -11,15 +12,59 @@ from basic_memory.mcp.server import mcp
|
||||
# Fields in BasicMemoryConfig that contain secrets and must never be surfaced.
|
||||
_SECRET_FIELDS = frozenset({"cloud_api_key"})
|
||||
|
||||
# Fields whose values are URLs that may embed user:password credentials.
|
||||
# The userinfo component is stripped before surfacing.
|
||||
_URL_FIELDS = frozenset({"database_url"})
|
||||
|
||||
|
||||
def _redact_url(url: str) -> str:
|
||||
"""Strip the userinfo (user:password) from a URL string.
|
||||
|
||||
Replaces any credentials with *** so the host/path remain visible for
|
||||
diagnostics (e.g. ``postgresql://***@localhost/mydb``). If the value
|
||||
cannot be parsed as a URL it is returned unchanged.
|
||||
"""
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
except Exception: # pragma: no cover — urlparse is very permissive
|
||||
return url
|
||||
|
||||
if not parsed.hostname:
|
||||
# Not a meaningful URL (e.g. a bare file path); leave it alone.
|
||||
return url
|
||||
|
||||
if parsed.username or parsed.password:
|
||||
# Rebuild with credentials replaced by a placeholder.
|
||||
netloc = f"***@{parsed.hostname}"
|
||||
if parsed.port:
|
||||
netloc = f"{netloc}:{parsed.port}"
|
||||
redacted = parsed._replace(netloc=netloc)
|
||||
return urlunparse(redacted)
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def _redact_config(raw: dict) -> dict:
|
||||
"""Return a copy of the raw config dict with secret fields removed.
|
||||
|
||||
Only top-level keys are redacted. Nested secret-looking keys within
|
||||
project entries are not currently present, but the pattern is explicit
|
||||
so it is easy to extend.
|
||||
- Keys in ``_SECRET_FIELDS`` are dropped entirely.
|
||||
- Keys in ``_URL_FIELDS`` have their userinfo component stripped so that
|
||||
host and database name remain visible for diagnostics.
|
||||
|
||||
Only top-level keys are processed. Nested keys within project entries are
|
||||
not currently credential-bearing, but the two sets make the pattern easy
|
||||
to extend.
|
||||
"""
|
||||
return {k: v for k, v in raw.items() if k not in _SECRET_FIELDS}
|
||||
result: dict = {}
|
||||
for k, v in raw.items():
|
||||
if k in _SECRET_FIELDS:
|
||||
# Drop entirely — value has no diagnostic value.
|
||||
continue
|
||||
if k in _URL_FIELDS and isinstance(v, str):
|
||||
result[k] = _redact_url(v)
|
||||
else:
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
|
||||
Reference in New Issue
Block a user