CVE-2026-44969: dbt-mcp: Tool Arguments Including SQL Queries and Credentials Logged in Plaintext Without Redaction When File Logging Is Enabled
Discovered through manual source code review. Verified by PoC execution against a local dbt-mcp v1.15.1 installation.
Summary
DbtMCP.calltool() in src/dbtmcp/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 DBTMCPSERVERFILELOGGING=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 {endtime - starttime}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 DBTMCPSERVERFILELOGGING=true (a documented feature in the project README). The log file location is resolved by configurefilelogging(), 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 poc4toolargslogged.py Vulnerable code: src/dbtmcp/mcp/server.py line 67, 77-79 configurefilelogging(): src/dbtmcp/telemetry/logging.py
import logging from pathlib import Path
LOGFILENAME = "dbt-mcp.log"
def configurefilelogging(loglevel: int = logging.INFO) -> Path: """Reproduction of configurefilelogging() from telemetry/logging.py.""" modulepath = Path(file).resolve().parent home = Path.home().resolve() for candidate in [modulepath, modulepath.parents]: if (candidate / ".git").exists() or (candidate / "pyproject.toml").exists() or candidate == home: reporoot = candidate break logpath = reporoot / LOGFILENAME rootlogger = logging.getLogger() rootlogger.setLevel(loglevel) filehandler = logging.FileHandler(logpath, encoding="utf-8") filehandler.setLevel(loglevel) filehandler.setFormatter( logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s") ) rootlogger.addHandler(filehandler) return logpath
logpath = configurefilelogging() serverlogger = logging.getLogger("dbtmcp.mcp.server")
Exact log statements from server.py line 67 and line 77-79 name = "show" arguments = {"sqlquery": "SELECT ssn, creditcardnumber, salary FROM customers WHERE id = 42", "limit": 5} serverlogger.info(f"Calling tool: {name} with arguments: {arguments}")
name2 = "run" arguments2 = {"nodeselection": "sensitivemodel", "vars": '{"dbpassword": "hunter2", "apikey": "sk-prod-abc123xyz"}', "isfullrefresh": False} serverlogger.info(f"Calling tool: {name2} with arguments: {arguments2}")
Verify file contents lines = logpath.readtext(encoding="utf-8").splitlines() poclines = [l for l in lines if "dbtmcp.mcp.server" in l] print(f"[log file: {logpath}]") for line in poclines: print(f" {line}")
keywords = ["ssn", "creditcardnumber", "salary", "dbpassword", "apikey"] found = [kw for kw in keywords if any(kw in l for l in poclines)] if found: print(f"\n[CONFIRMED] Sensitive keywords in plaintext log: {found}") print(f"[CONFIRMED] No redaction applied. File persists at {logpath}")
Expected log file entries:
2026-04-27 ... INFO [dbtmcp.mcp.server] Calling tool: show with arguments: {'sqlquery': 'SELECT ssn, creditcardnumber, salary FROM customers', 'limit': 5}
2026-04-27 ... INFO [dbtmcp.mcp.server] Calling tool: run with arguments: {'nodeselection': 'sensitivemodel', 'vars': '{"dbpassword":"hunter2","apikey":"sk-prod-abc123"}', 'isfullrefresh': False}
[CONFIRMED] Sensitive keywords in plaintext log: ['ssn', 'creditcardnumber', 'salary', 'dbpassword', 'apikey'] [CONFIRMED] No redaction applied.
<img width="3798" height="462" alt="image" src="https://github.com/user-attachments/assets/b4c23a93-b3d3-4b7f-ba46-3d4a324d609f" />
Impact
Directly proven by this PoC:
- When DBTMCPSERVERFILELOGGING=true, the full arguments dict of every tool call — including sqlquery, vars, and nodeselection — 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 LOGREDACT = frozenset({"sqlquery", "vars"})
def safeargs(arguments: dict) -> dict: return {k: "redacted" if k in LOGREDACT else v for k, v in arguments.items()}
server.py line 67: logger.info(f"Calling tool: {name} with arguments: {safeargs(arguments)}")
server.py lines 77-79: logger.error( f"Error calling tool: {name} with arguments: {safeargs(arguments)} " f"in {endtime - starttime}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.
Other sources
dbt-mcp is a Model Context Protocol server for interacting with dbt. Prior to 1.17.1, DbtMCP.calltool() in src/dbtmcp/mcp/server.py logged the raw arguments dictionary at INFO level before each tool call and at ERROR level on exceptions, and configurefilelogging() wrote those records to dbt-mcp.log when DBTMCPSERVERFILELOGGING=true, preserving sensitive sqlquery, vars, and nodeselection values in plaintext without automatic rotation or deletion. This issue is fixed in version 1.17.1.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/dbt-mcpto a version that resolves this vulnerability.Fixed in 1.17.1 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 1.17.1 - Configuration
Set DBT_MCP_SERVER_FILE_LOGGING=false to prevent dbt-mcp.log plaintext persistence of tool arguments (including sql_query/vars/node_selection) when file logging is enabled.
dbt-mcp file logging DBT_MCP_SERVER_FILE_LOGGING = false - Configuration
Configure logging redaction so that at least these argument keys are redacted before logging: sql_query and vars (as shown by _LOG_REDACT).
dbt-mcp server log redaction _LOG_REDACT = {"sql_query","vars"} - Configuration
Reduce the default log level for the file handler to WARNING so normal-operation INFO records (which include arguments) are not persisted.
dbt-mcp file handler logging level log_level = WARNING
Event History
Frequently Asked Questions
What is the severity of CVE-2026-44969?
CVE-2026-44969 is identified as a moderate severity vulnerability due to improper logging of sensitive information.
How do I fix CVE-2026-44969?
To mitigate CVE-2026-44969, upgrade dbt-mcp to version 1.17.1 or later.
What software is affected by CVE-2026-44969?
CVE-2026-44969 affects dbt-mcp versions up to and including 1.17.0.
What is the nature of CVE-2026-44969?
CVE-2026-44969 is a vulnerability that allows sensitive arguments to be logged at the INFO level, potentially exposing them.
When was CVE-2026-44969 discovered?
CVE-2026-44969 was discovered through a manual source code review.