system: OPERATIONAL
← back to all hacks
AGENTS MEDIUM NEW

dbt-mcp argument injection: agent inputs become dbt CLI flags

A dbt MCP server flaw fixed on 2026-07-16 let an MCP client smuggle dbt global flags like --profiles-dir through tool parameters, reaching the CLI even though shell=False was in place.

2026-07-22 // 6 min affects: dbt-mcp, mcp-servers, dbt, ai-agents, data-pipelines

What is this?

On 2026-07-16, dbt Labs published an advisory for an argument-injection flaw in dbt-mcp, the Model Context Protocol server that lets an LLM agent drive the dbt command-line tool. In releases before 1.17.1, two tool parameters that an MCP client controls — the node selection and the resource type — were passed through to the dbt subprocess without being separated from dbt’s own option parser. An MCP client could therefore make those parameters look like dbt global flags and change how dbt ran.

The flaw is worth reading not because it is exotic but because it is ordinary. It is a classic command-line argument-injection bug (CWE-88) that reappears in the agent world, where the “user” supplying those parameters is often a language model that a third party can influence through indirect prompt injection.

How it works

The server built a dbt invocation by appending caller-supplied strings to the argument list of subprocess.Popen(...) with shell=False. Setting shell=False is the correct baseline: it means the operating system does not run a shell, so shell metacharacters (;, |, backticks, $( )) are never interpreted and cannot chain commands. That defense holds here.

What shell=False does not do is stop a value from being read as another flag by the program you are launching. dbt accepts global options such as --profiles-dir, --project-dir, and --target. If a value the agent controls is placed into the argument list as its own token, dbt’s parser sees a new option rather than data. In dbt-mcp, the node-selection and resource-type parameters flowed into that list, so an MCP client could steer dbt to a configuration directory of its choosing rather than the operator’s.

The mental model:

# shell=False blocks ONE thing, not the other

  Shell metacharacter injection   ->  BLOCKED by shell=False
    (";", "|", "$(...)" never reach a shell)

  Argument / flag injection       ->  NOT blocked by shell=False
    (a controlled token is parsed by dbt as --profiles-dir / --target)

The impact is confidentiality and integrity, not remote code execution: redirecting --profiles-dir points dbt at a different profiles.yml, which defines database connections and targets. That is why the advisory is scored as a medium-severity, local, high-complexity issue — the attacker needs to control the tool parameters and needs a reachable alternate configuration, but the payoff is running dbt against connections the operator never intended. No working payload is reproduced here; the point is the parsing behaviour, not a recipe.

Why it matters

In a non-agent script, “the caller controls the arguments” is often acceptable, because the caller is trusted code. An MCP server inverts that assumption. The caller is an LLM, and in real deployments the LLM’s tool arguments can be shaped by content it read — a ticket, a README, a table description, a webpage — any of which may carry an indirect prompt injection. A parameter that a developer imagines as “a model name” becomes an untrusted channel that reaches a subprocess.

This places the bug squarely in the agent tool-wrapper class that keeps recurring across the ecosystem: the model’s structured inputs are treated as safe data on the server side, when they should be treated as adversarial. It maps to Excessive Agency and insecure tool design in the OWASP LLM Top 10. The lesson generalises to any MCP server that shells out — dbt, git, package managers, cloud CLIs — where option-bearing tools are one mis-placed token away from being repurposed.

Defenses

The fix shipped in dbt-mcp 1.17.1; upgrading is the first step. Beyond patching, the durable mitigations for this whole class are:

  1. Terminate option parsing. Insert a -- sentinel between flags and positional data so the child program stops interpreting later tokens as options, or pass values through interfaces that are never treated as flags. This is the single most reliable structural fix for argument injection.
  2. Allowlist, don’t sanitise. Validate agent-controlled parameters against an explicit set of permitted values (known model names, known resource types) and reject anything else, rather than trying to strip dangerous characters.
  3. Treat MCP tool arguments as untrusted input. Assume any string an LLM passes may have been steered by content it ingested. Apply input validation at the tool boundary as if it came from an anonymous internet user.
  4. Pin the configuration the subprocess can see. Set --profiles-dir, --project-dir, and --target from server-side configuration or environment, and refuse to let request parameters override them. Run the subprocess with least privilege and a constrained working directory.
  5. Log and review the final argv. Emit the exact argument vector each tool run produces, so anomalous flags are visible in monitoring and incident response.

Status

ItemReferenceDateNotes
dbt-mcp argument injectionCVE-2026-44968 / GHSA-xpww-f6pm-cfhq2026-07-16CVSS 6.3 (medium), CWE-88; affects dbt-mcp < 1.17.1
Fixed releasedbt-mcp v1.17.12026-07-16node_selection / resource_type no longer reach dbt as options
ClassOWASP LLM Top 10 (2025)2025Excessive Agency / insecure tool design

The vulnerability is publicly disclosed and patched; the details above come from the CVE record and the vendor advisory. The takeaway is not specific to dbt: any agent tool that appends model-supplied values to a subprocess should assume shell=False stops command chaining but not flag injection, and design the boundary accordingly.

Sources