From c568094b8c8b8e68194fe2a23563be419d094a3f Mon Sep 17 00:00:00 2001 From: Van Vuong Ngo <20791239+vanvuongngo@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:15:03 +0100 Subject: [PATCH] fix: do not log db user/password (#312) * fix: security vulnerability - exposed sensitve database credentials in logs * add comment * fix: mask credentials of the postgeSQL connection string --- hindsight-api/hindsight_api/banner.py | 4 +++- hindsight-api/hindsight_api/engine/memory_engine.py | 3 ++- hindsight-api/hindsight_api/migrations.py | 3 ++- hindsight-api/hindsight_api/utils.py | 12 ++++++++++++ 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 hindsight-api/hindsight_api/utils.py diff --git a/hindsight-api/hindsight_api/banner.py b/hindsight-api/hindsight_api/banner.py index 252dda47..ca9bf147 100644 --- a/hindsight-api/hindsight_api/banner.py +++ b/hindsight-api/hindsight_api/banner.py @@ -4,6 +4,8 @@ Banner display for Hindsight API startup. Shows the logo and tagline with gradient colors. """ +from .utils import mask_network_location + # Gradient colors: #0074d9 -> #009296 GRADIENT_START = (0, 116, 217) # #0074d9 GRADIENT_END = (0, 146, 150) # #009296 @@ -90,7 +92,7 @@ def print_startup_info( if version: print(f" {dim('Version:')} {color(f'v{version}', 0.1)}") print(f" {dim('URL:')} {color(f'http://{host}:{port}', 0.2)}") - print(f" {dim('Database:')} {color(database_url, 0.4)}") + print(f" {dim('Database:')} {color(mask_network_location(database_url), 0.4)}") print(f" {dim('LLM:')} {color(f'{llm_provider} / {llm_model}', 0.6)}") print(f" {dim('Embeddings:')} {color(embeddings_provider, 0.8)}") print(f" {dim('Reranker:')} {color(reranker_provider, 1.0)}") diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index 170a1e67..3c9aa0b5 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Any from ..config import get_config from ..metrics import get_metrics_collector +from ..utils import mask_network_location from .db_budget import budgeted_operation # Context variable for current schema (async-safe, per-task isolation) @@ -976,7 +977,7 @@ class MemoryEngine(MemoryEngineInterface): except Exception as e: logger.warning(f"Failed to run schema migrations: {e}") - logger.info(f"Connecting to PostgreSQL at {self.db_url}") + logger.info(f"Connecting to PostgreSQL at {mask_network_location(self.db_url)}") # Create connection pool # For read-heavy workloads with many parallel think/search operations, diff --git a/hindsight-api/hindsight_api/migrations.py b/hindsight-api/hindsight_api/migrations.py index d595dc07..39dcde8e 100644 --- a/hindsight-api/hindsight_api/migrations.py +++ b/hindsight-api/hindsight_api/migrations.py @@ -24,6 +24,7 @@ from alembic import command from alembic.config import Config from alembic.script.revision import ResolutionError from sqlalchemy import create_engine, text +from .utils import mask_network_location logger = logging.getLogger(__name__) @@ -54,7 +55,7 @@ def _run_migrations_internal(database_url: str, script_location: str, schema: st """ schema_name = schema or "public" logger.info(f"Running database migrations to head for schema '{schema_name}'...") - logger.info(f"Database URL: {database_url}") + logger.info(f"Database URL: {mask_network_location(database_url)}") logger.info(f"Script location: {script_location}") # Create Alembic configuration programmatically (no alembic.ini needed) diff --git a/hindsight-api/hindsight_api/utils.py b/hindsight-api/hindsight_api/utils.py new file mode 100644 index 00000000..1d92ce24 --- /dev/null +++ b/hindsight-api/hindsight_api/utils.py @@ -0,0 +1,12 @@ +from urllib.parse import urlparse, urlunparse + +def mask_network_location(url): + if not url: + return url + parsed_url = urlparse(url) + masked_network_location = parsed_url.hostname or "" + if parsed_url.port: + masked_network_location += f":{parsed_url.port}" + if parsed_url.username or parsed_url.password: + masked_network_location = f"***:***@{masked_network_location}" + return urlunparse(parsed_url._replace(netloc=masked_network_location)) \ No newline at end of file