GitHub Advisoryadvisory · PyPI
GHSA-7xgw-6qf3-7w59 dbt MCP Server Logs Tool Arguments Including SQL Queries and Credentials in Plaintext Without Redaction When File Logging Is Enabled
LOW
Official page ↗
- Aliases
- CVE-2026-44969
- Ecosystem
- PyPI
- Packages
- dbt-mcp
- Severity
- Low
- CVSS vector
- CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:N/A:N
- Weaknesses
- CWE-532
- Reviewed
- 2026-05-14
Affected packages
• PyPI dbt-mcp: < 1.17.1 → fixed in 1.17.1
Description
*Discovered through manual source code review. Verified by PoC execution against a local dbt-mcp v1.15.1 installation.*
Summary
`DbtMCP.call_tool()` in `src/dbt_mcp/mcp/server.py` logs the complete raw `arguments` dictionary at `INFO` level on every tool invocation (line 67) and again at `ERROR` level if the call raises an exception (lines 77–79). No field is redacted before logging. When the documented `DBT_MCP_SERVER_FILE_LOGGING=true` feature is enabled, these log records are written to `dbt-mcp.log` in the project root directory as plaintext. Sensitive data — raw SQL queries, `--vars` payloads carrying credentials, node selectors — persists on disk indefinitely with no automatic rotation or deletion.
Details
Vulnerable log statements (`server.py`):
```python
Line 67 — emitted before every tool execution
logger.info(f"Calling tool: {name} with arguments: {arguments}")
Lines 77–79 — emitted if the tool raises an exception (double-logging on failure)
logger.error(
f"Error calling tool: {name} with arguments: {arguments} "
f"in {end_time - start_time}ms: {e}"
)
```
`arguments` is the raw Python dict received from the MCP client. It is string-interpolated directly into the log message. On a tool call that raises an exception, the same dict is logged twice — once at INFO and once at ERROR.
File logging is activated by `DBT_MCP_SERVER_FILE_LOGGING=true` (a documented feature in the project README). The log file location is resolved by `configure_file_logging()`, which walks up the directory tree from `__file__` looking for `.git` or `pyproject.toml`, falling back to `$HOME`. Arguments are also emitted to stderr by the default stream handler regardless of file logging state.
PoC
MCP client script — triggers real tool calls and verifies log file contents:
```python
!/usr/bin/env python3
poc4_tool_args_logged.py
Vulnerable code: src/dbt_mcp/mcp/server.py line 67, 77-79
configure_file_logging(): src/dbt_mcp/telemetry/logging.py
import logging
from pathlib import Path
LOG_FILENAME = "dbt-mcp.log"
def configure_file_logging(log_level: int = logging.INFO) -> Path:
"""Reproduction of configure_file_logging() from telemetry/logging.py."""
module_path = Path(__file__).resolve().parent
home = Path.home().resolve()
for candidate in [module_path, *module_path.parents]:
if (candidate / ".git").exists() or (candidate / "pyproject.toml").exists() or candidate == home:
repo_root = candidate
break
log_path = repo_root / LOG_FILENAME
root_logger = logging.getLogger()
root_logger.setLevel(log_level)
file_handler = logging.FileHandler(log_path, encoding="utf-8")
file_handler.setLevel(log_level)
file_handler.setFormatter(
logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s")
)
root_logger.addHandler(file_handler)
return log_path
log_path = configure_file_logging()
server_logger = logging.getLogger("dbt_mcp.mcp.server")
Exact log statements from server.py line 67 and line 77-79
name = "show"
arguments = {"sql_query": "SELECT ssn, credit_card_number, salary FROM customers WHERE id = 42", "limit": 5}
server_logger.info(f"Calling tool: {name} with arguments: {arguments}")
name2 = "run"
arguments2 = {"node_selection": "sensitive_model", "vars": '{"db_password": "hunter2", "api_key": "sk-prod-abc123xyz"}', "is_full_refresh": False}
server_logger.info(f"Calling tool: {name2} with arguments: {arguments2}")
Verify file contents
lines = log_path.read_text(encoding="utf-8").splitlines()
poc_lines = [l for l in lines if "dbt_mcp.mcp.server" in l]
print(f"[log file: {log_path}]")
for line in poc_lines:
print(f" {line}")
keywords = ["ssn", "credit_card_number", "salary", "db_password", "api_key"]
found = [kw for kw in keywords if any(kw in l for l in poc_lines)]
if found:
print(f"\n[CONFIRMED] Sensitive keywords in plaintext log: {found}")
print(f"[CONFIRMED] No redaction applied. File persists at {log_path}")
```
Expected log file entries:
````
2026-04-27 ... INFO [dbt_mcp.mcp.server] Calling tool: show with arguments:
{'sql_query': 'SELECT ssn, credit_card_number, salary FROM customers', 'limit': 5}
2026-04-27 ... INFO [dbt_mcp.mcp.server] Calling tool: run with arguments:
{'node_selection': 'sensitive_model',
'vars': '{"db_password":"hunter2","api_key":"sk-prod-abc123"}',
'is_full_refresh': False}
[CONFIRMED] Sensitive keywords in plaintext log: ['ssn', 'credit_card_number', 'salary', 'db_password', 'api_key']
[CONFIRMED] No redaction applied.
````
Impact
Directly proven by this PoC:
• When `DBT_MCP_SERVER_FILE_LOGGING=true`, the full `arguments` dict of every tool call — including `sql_query`, `vars`, and `node_selection` — is written to `dbt-mcp.log` in plaintext on every invocation.
• A tool call that raises an exception produces two log entries with the same sensitive content (INFO + ERROR double-logging).
• The log file has no automatic rotation, expiry, or access restriction beyond filesystem permissions.
Combined with Advisory 3 (telemetry), a single `show` tool call containing PII produces one telemetry transmission to dbt Labs and one (or two, on failure) persistent log entries on disk.
Remediation
redact known-sensitive argument values before logging:
```python
_LOG_REDACT = frozenset({"sql_query", "vars"})
def _safe_args(arguments: dict) -> dict:
return {k: "*redacted*" if k in _LOG_REDACT else v
for k, v in arguments.items()}
server.py line 67:
logger.info(f"Calling tool: {name} with arguments: {_safe_args(arguments)}")
server.py lines 77-79:
logger.error(
f"Error calling tool: {name} with arguments: {_safe_args(arguments)} "
f"in {end_time - start_time}ms: {e}"
)
```
log argument keys only:
```python
logger.info(f"Calling tool: {name} with argument keys: {list(arguments.keys())}")
```
File logging: Consider reducing the default log level for the file handler to `WARNING` so that normal-operation INFO records (which include arguments) are not persisted. Sensitive content would only appear in file logs on error.
Related
Alias · CVE (1)
- CVE-2026-44969 dbt-mcp is a Model Context Protocol server for interacting with dbt. vulnerability
Weakness · CWE (1)
- CWE-532 Insertion of Sensitive Information into Log File weakness