NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

GHSA-xrmj-5g4g-8987: @dynatrace-oss/dynatrace-mcp-server has a workflow template injection via create_workflow_for_notification

mediumCVSS 3.1 4.2

The create_workflow_for_notification tool in the Dynatrace MCP server (@dynatrace-oss/dynatrace-mcp-server) copies teamName, problemType and channel into a Dynatrace Workflow without escaping. Jinja expressions such as {{ event() }} then run when the workflow fires, leaking event data through a workflow that outlives the session. Fixed in 2.0.0, which removes the tool.

Published Jul 31, 2026 · Analysis Oct 25, 2026

What an attacker can do

The tool in src/capabilities/create-workflow-for-problem-notification.ts builds a workflow with a dynatrace.slack:slack-send-message action. Dynatrace evaluates {{ ... }} in action inputs at runtime, and that action is not on the exception list. The three string parameters have no pattern validation. channel sits inside an existing {{ "..." }} expression, so a closing quote lets an attacker run arbitrary Jinja in the destination field.

The reporter set teamName to {{ event() }} against a real tenant. The stored workflow kept the expression verbatim, and a manual run resolved it to the full event object in the message. Only the placeholder Slack connection id stopped delivery.

The workflow persists after the session ends, the operator's credentials are revoked or the server is removed, and it fires on every matching problem until someone deletes it in the Workflows app. isPrivate defaults to false, so the whole tenant can see it.

An attacker needs a prompt injection that makes the agent call the tool, or an operator who approves the prompt. That prompt shows {{ event() }} as plain text with no hint it is a template, and does not show visibility. The score is moderate: low confidentiality and integrity, high complexity, user interaction.

Who is affected

@dynatrace-oss/dynatrace-mcp-server before 2.0.0, where the create_workflow_for_notification tool exists. Servers that never expose this tool to an agent, or whose operators deny it, are not exposed to new workflows. Also check for workflows already created by the tool.

npm ls @dynatrace-oss/dynatrace-mcp-server
npm ls -g @dynatrace-oss/dynatrace-mcp-server

@dynatrace-oss/dynatrace-mcp-server

Ecosystem
npm
Affected
< 2.0.0
Fixed in
2.0.0
Check
npm ls @dynatrace-oss/dynatrace-mcp-server

How to fix it

Upgrade to 2.0.0:

npm install @dynatrace-oss/[email protected]

The 2.0.0 release removes the create_workflow_for_notification and make_workflow_public tools, and marks this as a breaking change. Anything that relied on them needs another route to create workflows.

Upgrading does not delete workflows that were already made. Open the Workflows app in the Dynatrace tenant, find ones titled like [MCP POC] Notify team ..., and inspect or delete any with {{ in the team name, problem type or channel.

How to tell if you were hit

In the Dynatrace Workflows app, search for workflows whose title begins with [MCP POC] Notify team and check the send_notification task. A message or channel containing {{ event() }}, {{ environment() }} or other expressions outside the standard display_id, event.status and event.id fields is suspect, as is a channel that is not one you own. Review the Automation API output for workflows with isPrivate false. In agent logs, look for create_workflow_for_notification calls with braces in the arguments. Beam records agent MCP tool calls, which helps find those.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

Summary

A template injection vulnerability in the create_workflow_for_notification tool lets a caller embed Jinja2 expressions that the Dynatrace workflow engine evaluates at runtime, exfiltrating event data to attacker-controlled destinations through a workflow that persists in the tenant after the MCP session ends.

Details

The create_workflow_for_notification tool interpolates three caller-supplied parameters (teamName, problemType, channel) directly into a Dynatrace Workflow definition. Dynatrace Workflows use Jinja2 templating: per the official documentation, {{ ... }} expressions in action inputs are evaluated at workflow runtime for every action except Run Javascript (which is carved out specifically to avoid code injection). A caller can therefore supply, for example, teamName = "{{ event() }}" and have the workflow engine evaluate that expression at runtime, serialising the full event object into the message body delivered to the Slack channel.

The vulnerable code is in src/capabilities/create-workflow-for-problem-notification.ts, lines 82-99:

let notificationWorkflow: WorkflowCreate = {
  title: `[MCP POC] Notify team ${teamName} on problem of type ${problemType}`,
  description: `Automatically created workflow to notify team ${teamName} on problems of type ${problemType} - ...`,
  isPrivate: isPrivate,
  type: 'SIMPLE',
  tasks: {
    send_notification: {
      name: 'Send notification',
      action: 'dynatrace.slack:slack-send-message',
      description: 'Sends a notification to a Slack channel',
      input: {
        connectionId: 'slack-connection-id',
        channel: `{{ \"${channel}\" }}`,        // <-- channel sits inside {{ }}
        message: `🚨 Alert for Team ${teamName}\n*Problem Type*: ${problemType}\n` +
                 `*Problem ID*: {{ event()["display_id"] }}\n*Status*: {{ event()["event.status"] }}\n` +
                 `<{{ environment().url }}/ui/apps/.../problem/{{ event()["event.id"] }}|Click here>`,
      },
      active: true,
    },
  },
};

The action used is dynatrace.slack:slack-send-message, which is not in the documented Jinja-expression exception list. Its inputs are evaluated at workflow runtime.

The schema in src/index.ts:1052-1070 registers teamName, problemType, and channel as z.string().optional() with no pattern validation. The isPrivate parameter has .default(false), so created workflows are visible tenant-wide unless the caller explicitly sets it.

The approval prompt at src/index.ts:1069-1072:

const approved = await requestHumanApproval(
  `Create a workflow for notifying team ${teamName} via ${channel} about ${problemType} problems`,
);

Renders {{ event() }} literally to the operator with no indication that it will be templated, and does not surface the workflow's visibility.

The workflow is persistent: it remains in the tenant after the MCP session ends, after the operator's MCP credentials are revoked, and after the MCP server is uninstalled. It fires on every matching problem until manually deleted from the Workflows app. The channel parameter is uniquely dangerous because it is interpolated inside an existing {{ "..." }} expression context - close the string with " and you can run arbitrary Jinja expressions in the destination field itself.

PoC

Tested end-to-end against a real Dynatrace tenant. The MCP server was run in stdio mode with the operator's Platform Token. A tools/call create_workflow_for_notification was sent with:

{
  "teamName":    "{{ event() }}",
  "problemType": "ERROR",
  "channel":     "#mcp-sec-poc",
  "isPrivate":   true
}

The operator approved the prompt (which read: "Create a workflow for notifying team {{ event() }} via #mcp-sec-poc about ERROR problems"). The MCP returned a workflow ID. Fetching the stored workflow body via the Dynatrace Automation API (GET /platform/automation/v1/workflows/<id>) showed the injected expression stored verbatim:

title:   "[MCP POC] Notify team {{ event() }} on problem of type ERROR"
message: "🚨 Alert for Team {{ event() }}\n*Problem Type*: ERROR\n*Problem ID*: {{ event()[\"display_id\"] }}\n..."
channel: '{{ "#mcp-sec-poc" }}'
action:  "dynatrace.slack:slack-send-message"

The workflow was then triggered manually via the "Run workflow" feature in the Dynatrace Workflows app, with a synthetic event payload {"display_id":"P-123","event.status":"OPEN","event.id":"abc-123"}. The execution log for the send_notification task shows the workflow engine evaluated the injected expressions at runtime. The "Input" tab for the executed action contains:

channel: #mcp-sec-poc
message: Alert for Team
         {'display_id': 'P-123',
          'event.status': 'OPEN',
          'event.id': 'abc-123'}
         *Problem Type*: ERROR
         *Problem ID*: P-123
         *Status*: OPEN
         <https://<tenant>.apps.dynatrace.com/ui/apps/.../problem/abc-123|Click here for details>

The injected {{ event() }} resolved to the actual event object before the Slack action was called. The Slack action then failed only because the workflow uses a hardcoded placeholder connectionId: 'slack-connection-id' that does not resolve to any real connection. If a real Slack connector had been configured, the message would have been delivered with the serialized event data in place of {{ event() }}.

This is end-to-end confirmation of the Jinja-evaluation chain.

Impact

A workflow created with a malicious template payload acts as a persistent exfiltration channel:

  • It fires on every matching problem indefinitely.
  • The injected template is evaluated at runtime and resolves to whatever the Jinja function returns (event() gives the full event object, environment() gives tenant metadata, plus other available functions per the Dynatrace Workflows documentation).
  • The resolved output is delivered to a destination the attacker controls (the channel parameter is also templated, and is even more dangerous because it is interpolated inside an existing {{ "..." }} expression context).
  • The workflow persists in the tenant after the MCP session, operator credentials, and MCP server are gone.
  • With isPrivate=false (the default), the workflow is visible to all tenant users.

Exploitation requires either prompt injection (an LLM under the MCP server's control is reading attacker-controlled data and follows an injection that calls create_workflow_for_notification with the malicious arguments) or operator carelessness (the approval prompt shows the raw team name literal and the operator clicks Approve without recognising {{ event() }} as a template fragment). Both are realistic in practice.

Credited to yotampe-pluto.

Questions

Does GHSA-xrmj-5g4g-8987 need a human to approve the tool call?

The tool asks for human approval, but the prompt shows the template text as a literal. The advisory says an operator who does not spot {{ event() }} may approve it, or prompt injection may trigger the call.

Does upgrading remove a malicious workflow already created?

No. Workflows persist in the tenant independent of the MCP server. You must find and delete them in the Dynatrace Workflows app.

How did 2.0.0 fix it?

By removing the create_workflow_for_notification tool, along with make_workflow_public, as listed in the release notes. That is a breaking change for users of those tools.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More Other MCP servers and tools vulnerabilities

All Other MCP servers and tools entries