Root cause
Injection & unsafe execution
Add timeframe parameter to list_vulnerabilities and get_kubernetes_events tools
AI-assisted change: GitHub Copilot
How AI contributed: New attack surfacePublished Jul 31, 2026 in
dynatrace-oss/dynatrace-mcp
The AI-written change added an optional free-form timeframe parameter to the list_vulnerabilities and get_kubernetes_events tools and interpolated the value directly into the fetch window of the DQL statements those tools execute. Nothing validated the text, so a caller could append extra query stages or break out of the query string. Any client able to invoke these MCP tools could therefore run arbitrary DQL against the connected Dynatrace environment.
The tool schemas accept timeframe as an unconstrained string, and list-vulnerabilities.ts and get-events-for-cluster.ts build their statements by template interpolation rather than binding the value, so the duration text becomes part of the executed query. The fix validates the timeframe against a duration pattern, rejects pipes and newlines in filters, and escapes quoted identifier values before the statement is built.
Root cause
Add timeframe parameter to list_vulnerabilities and get_kubernetes_events tools
AI-assisted change: GitHub Copilot
Fix
The fix validates timeframes and filters and escapes quoted identifiers before building DQL, preventing input from adding query stages or breaking out of strings.
AI-assisted fix: Claude Code · Christian Kreuzberger
Case facts
@@ -354,8 +354,15 @@ const main = async () => { tool( 'list_vulnerabilities', 'List Vulnerabilities',- 'Retrieve all active (non-muted) vulnerabilities from Dynatrace for the last 30 days. An additional filter can be provided using DQL filter (filter for a specific entity type and id).',+ 'Retrieve all active (non-muted) vulnerabilities from Dynatrace. An additional filter can be provided using DQL filter (filter for a specific entity type and id).', {+ timeframe: z+ .string()+ .optional()+ .default('30d')+ .describe(+ 'Timeframe to query vulnerabilities (e.g., "12h", "24h", "7d", "30d", "90d"). Default: "30d". Supports hours (h) and days (d).',+ ), riskScore: z .number() .optional()@@ -376,27 +383,27 @@ const main = async () => { { readOnlyHint: true, },- async ({ riskScore, additionalFilter, maxVulnerabilitiesToDisplay }) => {+ async ({ timeframe, riskScore, additionalFilter, maxVulnerabilitiesToDisplay }) => { const dtClient = await createAuthenticatedHttpClient( scopesBase.concat( 'storage:events:read', 'storage:buckets:read', 'storage:security.events:read', // Read Security events from Grail ), );- const result = await listVulnerabilities(dtClient, additionalFilter, riskScore);+ const result = await listVulnerabilities(dtClient, additionalFilter, riskScore, timeframe); if (!result || result.length === 0) {- return 'No vulnerabilities found in the last 30 days';+ return `No vulnerabilities found in the last ${timeframe}`; }- let resp = `Found ${result.length} vulnerabilities in the last 30 days! Displaying the top ${maxVulnerabilitiesToDisplay} vulnerabilities:\n`;+ let resp = `Found ${result.length} vulnerabilities in the last ${timeframe}! Displaying the top ${maxVulnerabilitiesToDisplay} vulnerabilities:\n`; result.slice(0, maxVulnerabilitiesToDisplay).forEach((vulnerability) => { resp += `\n* ${vulnerability}`; }); resp += `\nNext Steps:` + `\n1. For specific vulnerabilities, first always fetch more details using the "execute_dql" tool and the following query:- "fetch security.events, from: now()-30d, to: now()+ "fetch security.events, from: now()-${timeframe}, to: now() | filter event.provider=="Dynatrace" AND event.type=="VULNERABILITY_STATE_REPORT_EVENT" AND event.level=="ENTITY"@@ -1011,6 +1018,13 @@ const main = async () => { 'Get Kubernetes Events', 'Get all events from a specific Kubernetes (K8s) cluster', {+ timeframe: z+ .string()+ .optional()+ .default('24h')+ .describe(+ 'Timeframe to query events (e.g., "12h", "24h", "7d", "30d"). Default: "24h". Supports hours (h) and days (d).',+ ), clusterId: z .string() .optional()@@ -1046,12 +1060,12 @@ const main = async () => { { readOnlyHint: true, },- async ({ clusterId, kubernetesEntityId, eventType, maxEventsToDisplay }) => {+ async ({ timeframe, clusterId, kubernetesEntityId, eventType, maxEventsToDisplay }) => { const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:events:read'));- const result = await getEventsForCluster(dtClient, clusterId, kubernetesEntityId, eventType);+ const result = await getEventsForCluster(dtClient, clusterId, kubernetesEntityId, eventType, timeframe); if (result && result.records && result.records.length > 0) {- let resp = `Found ${result.records.length} events! Displaying the top ${maxEventsToDisplay} events:\n`;+ let resp = `Found ${result.records.length} events in the last ${timeframe}! Displaying the top ${maxEventsToDisplay} events:\n`; // iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems result.records.slice(0, maxEventsToDisplay).forEach((event) => { if (event) {@@ -1062,12 +1076,12 @@ const main = async () => { resp += `\nNext Steps:` + `\n1. Consider filtering by \`eventType\` to find specific events of interest.` +- `\n2. Use "execute_dql" tool with the following query to get more details about a specific event: "fetch events | filter event.id == \"<event-id>\""`;+ `\n2. Use "execute_dql" tool with the following query to get more details about a specific event: "fetch events, from: now()-${timeframe}, to: now() | filter event.id == \"<event-id>\""`; return resp; } @@ -1,9 +1,22 @@ import { HttpClient } from '@dynatrace-sdk/http-client'; import { executeDql } from './execute-dql'; -export const listVulnerabilities = async (dtClient: HttpClient, additionalFilter?: string, riskScore?: number) => {- // DQL query to fetch vulnerabilities with extended timeframe (last 30 days)- const dqlStatement = `fetch security.events+/**+ * List vulnerabilities from security.events+ * @param dtClient Dynatrace HTTP Client+ * @param additionalFilter Any additional DQL filter+ * @param riskScore Minimum risk score to filter vulnerabilities (default: 8.0)+ * @param timeframe Timeframe to query vulnerabilities (e.g., '24h', '7d', '30d'). Default: '30d'+ * @returns Array of formatted vulnerability strings+ */+export const listVulnerabilities = async (+ dtClient: HttpClient,+ additionalFilter?: string,+ riskScore?: number,+ timeframe: string = '30d',+) => {+ // DQL query to fetch vulnerabilities with configurable timeframe+ const dqlStatement = `fetch security.events, from: now()-${timeframe}, to: now() | filter dt.system.bucket=="default_securityevents_builtin" AND event.provider=="Dynatrace" AND event.type=="VULNERABILITY_STATE_REPORT_EVENT"@@ -1,13 +1,23 @@ import { HttpClient } from '@dynatrace-sdk/http-client'; import { executeDql } from './execute-dql'; +/**+ * Get events for a Kubernetes cluster+ * @param dtClient Dynatrace HTTP Client+ * @param clusterId The Kubernetes Cluster ID (k8s.cluster.uid)+ * @param kubernetesEntityId The Dynatrace Kubernetes Entity ID (dt.entity.kubernetes_cluster)+ * @param eventType Filter by specific event type+ * @param timeframe Timeframe to query events (e.g., '12h', '24h', '7d', '30d'). Default: '24h'+ * @returns DQL query result+ */ export const getEventsForCluster = async ( dtClient: HttpClient, clusterId: string, kubernetesEntityId: string, eventType: string,+ timeframe: string = '24h', ) => {- let dql = 'fetch events';+ let dql = `fetch events, from: now()-${timeframe}, to: now()`; if (!clusterId && !kubernetesEntityId) { // If no clusterId or kubernetesEntityId is provided, return all kubernetes related events@@ -1,5 +1,6 @@ import { HttpClient } from '@dynatrace-sdk/http-client'; import { executeDql } from './execute-dql';+import { validateAdditionalFilter, validateTimeframe } from '../utils/dql-sanitize'; /** * List vulnerabilities from security.events@@ -15,6 +16,9 @@ export const listVulnerabilities = async ( riskScore?: number, timeframe: string = '30d', ) => {+ validateTimeframe(timeframe);+ if (additionalFilter) validateAdditionalFilter(additionalFilter);+ // DQL query to fetch vulnerabilities with configurable timeframe const dqlStatement = `fetch security.events, from: now()-${timeframe}, to: now() | filter dt.system.bucket=="default_securityevents_builtin"@@ -1,12 +1,16 @@ import { HttpClient } from '@dynatrace-sdk/http-client'; import { executeDql } from './execute-dql';+import { validateAdditionalFilter, validateTimeframe } from '../utils/dql-sanitize'; export const listExceptions = async ( dtClient: HttpClient, additionalFilter?: string, timeframe: string = '24h', maxResultRecords: number = 5000, ) => {+ validateTimeframe(timeframe);+ if (additionalFilter) validateAdditionalFilter(additionalFilter);+ // DQL statement to fetch exception data from user.events for the specified timeframe const dql = `fetch user.events, from: now()-${timeframe}, to: now() | filter isNotNull(exception.stack_trace)AI-assisted change fb17171e91782eb3adde0963d4aad3cb37eba880cb42e60748c2a6945a7e71dd · Fix 66f7c0b588fa495695b62125a240bfed864b58cbf5959086afaa05c4f9c6a5e3