1+ // Description: This script detects potentially exposed MCP servers by sending MCP initialization requests
2+ // Author: Daniel Santos (@bananabr)
3+
4+ var ScanRuleMetadata = Java . type ( "org.zaproxy.addon.commonlib.scanrules.ScanRuleMetadata" ) ;
5+ var CommonAlertTag = Java . type ( "org.zaproxy.addon.commonlib.CommonAlertTag" ) ;
6+
7+ function getMetadata ( ) {
8+ return ScanRuleMetadata . fromYaml ( `
9+ id: 100030
10+ name: Open MCP Server Detection
11+ description: >
12+ This script detects potentially exposed Model Context Protocol (MCP) servers
13+ by sending MCP initialization requests and analyzing responses for characteristic
14+ MCP protocol signatures.
15+ solution: >
16+ Ensure MCP servers are properly secured and not exposed to unauthorized access.
17+ Implement proper authentication and access controls for MCP endpoints.
18+ references:
19+ - https://spec.modelcontextprotocol.io/specification/
20+ - https://github.com/modelcontextprotocol/specification
21+ category: server
22+ risk: medium
23+ confidence: medium
24+ cweId: 200 # CWE-200: Information Exposure
25+ wascId: 13 # WASC-13: Information Leakage
26+ alertTags:
27+ ${ CommonAlertTag . OWASP_2021_A05_SEC_MISCONFIG . getTag ( ) } : ${ CommonAlertTag . OWASP_2021_A05_SEC_MISCONFIG . getValue ( ) }
28+ ${ CommonAlertTag . OWASP_2017_A06_SEC_MISCONFIG . getTag ( ) } : ${ CommonAlertTag . OWASP_2017_A06_SEC_MISCONFIG . getValue ( ) }
29+ status: alpha
30+ codeLink: https://github.com/zaproxy/community-scripts/blob/main/active/mcp_server_detector.js
31+ helpLink: https://www.zaproxy.org/docs/desktop/addons/community-scripts/
32+ ` ) ;
33+ }
34+
35+ /**
36+ * Scans a node for exposed MCP servers
37+ * @param as - ActiveScan object
38+ * @param msg - HttpMessage object
39+ */
40+ function scanNode ( as , msg ) {
41+ print ( 'MCP Server Detector: Scanning ' + msg . getRequestHeader ( ) . getURI ( ) . toString ( ) ) ;
42+
43+ // Check if the scan was stopped
44+ if ( as . isStop ( ) ) {
45+ return ;
46+ }
47+
48+ // Get the original URI
49+ var uri = msg . getRequestHeader ( ) . getURI ( ) ;
50+ var baseUrl = uri . getScheme ( ) + "://" + uri . getHost ( ) ;
51+ if ( uri . getPort ( ) !== - 1 ) {
52+ baseUrl += ":" + uri . getPort ( ) ;
53+ }
54+
55+ // Common MCP server endpoints to test
56+ var mcpEndpoints = [
57+ "/" , // Root path - Default for many MCP servers, @modelcontextprotocol/server-stdio
58+ "/mcp" , // Standard MCP path - Custom implementations, MCP reference servers
59+ "/mcp/" , // MCP with trailing slash - Web-based MCP servers, Express.js implementations
60+ "/api/mcp" , // API-style path - REST API wrappers, enterprise MCP gateways
61+ "/rpc" , // Generic RPC endpoint - JSON-RPC servers that support MCP, multi-protocol servers
62+ "/jsonrpc" , // JSON-RPC endpoint - Pure JSON-RPC implementations with MCP support
63+ "/mcp-server" , // Explicit server path - Standalone MCP server deployments, Docker containers
64+ "/v1/mcp" // Versioned API path - Versioned MCP APIs, enterprise/production deployments
65+ ] ;
66+
67+ // Add current path if it's not null or empty
68+ var currentPath = uri . getPath ( ) ;
69+ if ( currentPath && currentPath !== "/" && currentPath !== "" ) {
70+ mcpEndpoints . push ( currentPath ) ;
71+ }
72+
73+ // MCP initialization payload
74+ var mcpInitPayload = JSON . stringify ( {
75+ "jsonrpc" : "2.0" ,
76+ "id" : 1 ,
77+ "method" : "initialize" ,
78+ "params" : {
79+ "protocolVersion" : "2024-11-05" ,
80+ "capabilities" : {
81+ "roots" : {
82+ "listChanged" : true
83+ } ,
84+ "sampling" : { } ,
85+ "elicitation" : { }
86+ } ,
87+ "clientInfo" : {
88+ "name" : "ZAPActiveScript" ,
89+ "title" : "ZAP Open MCP Active Script" ,
90+ "version" : "1.0.0"
91+ }
92+ }
93+ } ) ;
94+
95+ // Test each potential MCP endpoint
96+ for ( var i = 0 ; i < mcpEndpoints . length ; i ++ ) {
97+ if ( as . isStop ( ) ) {
98+ return ;
99+ }
100+
101+ var endpoint = mcpEndpoints [ i ] ;
102+ var foundMcp = testMcpEndpoint ( as , msg , baseUrl + endpoint , mcpInitPayload ) ;
103+
104+ // Break out of loop if we found a vulnerable MCP server
105+ if ( foundMcp ) {
106+ print ( 'MCP Server Detector: Found vulnerable MCP server, stopping endpoint enumeration' ) ;
107+ break ;
108+ }
109+ }
110+ }
111+
112+ /**
113+ * Tests a specific endpoint for MCP server responses
114+ * @param as - ActiveScan object
115+ * @param originalMsg - Original HttpMessage
116+ * @param testUrl - URL to test
117+ * @param payload - MCP payload to send
118+ * @return boolean - true if MCP server found, false otherwise
119+ */
120+ function testMcpEndpoint ( as , originalMsg , testUrl , payload ) {
121+ try {
122+ print ( 'MCP Server Detector: Testing endpoint ' + testUrl ) ;
123+ var testMsg = originalMsg . cloneRequest ( ) ;
124+ var requestHeader = testMsg . getRequestHeader ( ) ;
125+
126+ // Set the new URL using Apache Commons HttpClient URI
127+ var HttpClientURI = Java . type ( "org.apache.commons.httpclient.URI" ) ;
128+ requestHeader . setURI ( new HttpClientURI ( testUrl , false ) ) ;
129+ requestHeader . setMethod ( "POST" ) ;
130+
131+ // Set appropriate headers
132+ requestHeader . setHeader ( "Accept" , "application/json, text/event-stream" ) ;
133+ requestHeader . setHeader ( "Content-Type" , "application/json" ) ;
134+
135+ // Set the request body
136+ testMsg . setRequestBody ( payload ) ;
137+
138+ // Send the request
139+ as . sendAndReceive ( testMsg , false , false ) ;
140+
141+ // Analyze the response and return whether MCP server was found
142+ return analyzeMcpResponse ( as , testMsg , payload ) ;
143+
144+ } catch ( e ) {
145+ print ( 'MCP Server Detector: Error testing endpoint ' + testUrl + ': ' + e ) ;
146+ return false ;
147+ }
148+ }
149+
150+ /**
151+ * Analyzes the response for MCP server indicators
152+ * @param as - ActiveScan object
153+ * @param msg - HttpMessage with response
154+ * @param originalPayload - Original payload sent
155+ * @return boolean - true if MCP server detected, false otherwise
156+ */
157+ function analyzeMcpResponse ( as , msg , originalPayload ) {
158+ var response = msg . getResponseBody ( ) . toString ( ) ;
159+ var responseHeader = msg . getResponseHeader ( ) ;
160+ var statusCode = responseHeader . getStatusCode ( ) ;
161+
162+ print ( 'MCP Server Detector: Analyzing response from ' + msg . getRequestHeader ( ) . getURI ( ) . toString ( ) ) ;
163+ print ( 'MCP Server Detector: Status Code: ' + statusCode ) ;
164+ print ( 'MCP Server Detector: Response length: ' + msg . getResponseBody ( ) . length ( ) ) ;
165+
166+ // Get response headers for additional analysis
167+ var contentType = responseHeader . getHeader ( "Content-Type" ) ;
168+ var mcpSessionId = responseHeader . getHeader ( "Mcp-Session-Id" ) ;
169+ var transferEncoding = responseHeader . getHeader ( "Transfer-Encoding" ) ;
170+ var server = responseHeader . getHeader ( "Server" ) ;
171+
172+ print ( 'MCP Server Detector: Content-Type: ' + contentType ) ;
173+ print ( 'MCP Server Detector: Mcp-Session-Id: ' + mcpSessionId ) ;
174+ print ( 'MCP Server Detector: Transfer-Encoding: ' + transferEncoding ) ;
175+
176+ // Analyze content types for MCP compliance
177+ var hasMcpHeaders = mcpSessionId !== null ;
178+ var hasEventStream = contentType !== null && contentType . indexOf ( "text/event-stream" ) !== - 1 ;
179+ var hasJsonResponse = contentType !== null && contentType . toLowerCase ( ) . indexOf ( "application/json" ) !== - 1 ;
180+
181+ // MCP servers MUST respond with either text/event-stream OR application/json for JSON-RPC requests
182+ var hasMcpCompliantContentType = hasEventStream || hasJsonResponse ;
183+
184+ // Skip analysis if no valid response and no MCP indicators
185+ if ( ! hasMcpHeaders && ! hasMcpCompliantContentType && ( response . length === 0 || statusCode !== 200 ) ) {
186+ return false ;
187+ }
188+
189+ // For 200 responses with MCP-compliant content types, proceed with analysis even if body is empty
190+ // (SSE streams might not have loaded the body yet)
191+ var shouldAnalyze = ( statusCode === 200 && hasMcpCompliantContentType ) || hasMcpHeaders || response . length > 0 ;
192+ if ( ! shouldAnalyze ) {
193+ return false ;
194+ }
195+
196+ // Debug: Log the first 200 characters of response for debugging
197+ var debugResponse = response . length > 200 ? response . substring ( 0 , 200 ) + "..." : response ;
198+ print ( 'MCP Server Detector: Response preview: ' + debugResponse ) ;
199+
200+ var isValidMcp = false ;
201+ var evidence = "" ;
202+ var confidence = 1 ; // Low confidence by default
203+ var risk = 1 ; // Low risk by default
204+
205+ // Strict MCP server validation according to specification requirements
206+
207+ // Case 1: SSE format - Content-Type is text/event-stream AND status 200 AND has Mcp-Session-Id header
208+ if ( hasEventStream && statusCode === 200 && hasMcpHeaders ) {
209+ isValidMcp = true ;
210+ confidence = 4 ; // Confirmed MCP SSE server
211+ risk = 3 ; // High risk - exposed MCP server
212+ evidence = "Confirmed MCP Server (SSE format): text/event-stream content type with Mcp-Session-Id header" ;
213+ }
214+ // Case 2: SSE format - Content-Type is text/event-stream AND status 200 (without MCP session header)
215+ else if ( hasEventStream && statusCode === 200 && ! hasMcpHeaders ) {
216+ isValidMcp = true ;
217+ confidence = 2 ; // Lower confidence without MCP session header
218+ risk = 2 ; // Medium risk - might be MCP server
219+ evidence = "Suspected MCP Server (SSE format): text/event-stream content type without Mcp-Session-Id header" ;
220+ }
221+ // Case 3: JSON format - Content-Type is application/json AND status 200 AND valid MCP initialize response structure
222+ else if ( hasJsonResponse && statusCode === 200 ) {
223+ // Parse JSON response to validate MCP structure
224+ var isValidMcpJson = false ;
225+ var jsonParseError = null ;
226+
227+ try {
228+ if ( response . length > 0 ) {
229+ var jsonResponse = JSON . parse ( response ) ;
230+
231+ // Check for valid MCP initialize response structure
232+ if ( jsonResponse &&
233+ jsonResponse . jsonrpc === "2.0" &&
234+ jsonResponse . id !== undefined &&
235+ jsonResponse . result &&
236+ jsonResponse . result . protocolVersion &&
237+ jsonResponse . result . capabilities &&
238+ jsonResponse . result . serverInfo ) {
239+ isValidMcpJson = true ;
240+ }
241+ }
242+ } catch ( e ) {
243+ jsonParseError = e . toString ( ) ;
244+ }
245+
246+ if ( isValidMcpJson ) {
247+ isValidMcp = true ;
248+ confidence = 4 ; // Confirmed MCP JSON server
249+ risk = 3 ; // High risk - exposed MCP server
250+ evidence = "Confirmed MCP Server (JSON format): Valid MCP initialize response with required structure " +
251+ "(jsonrpc: '2.0', id, result.protocolVersion, result.capabilities, result.serverInfo)" ;
252+ } else if ( jsonParseError ) {
253+ print ( 'MCP Server Detector: JSON parse error: ' + jsonParseError ) ;
254+ }
255+ }
256+
257+ // Only raise alert if we detected a valid MCP server
258+ if ( isValidMcp ) {
259+ // Add strict MCP specification validation details
260+ evidence += "\n\nMCP Specification Validation:" ;
261+ if ( hasEventStream && hasMcpHeaders && statusCode === 200 ) {
262+ evidence += "\n✓ SSE Format: text/event-stream + Mcp-Session-Id header + HTTP 200" ;
263+ }
264+ if ( hasJsonResponse && statusCode === 200 ) {
265+ evidence += "\n✓ JSON Format: application/json + HTTP 200 + Valid MCP response structure" ;
266+ }
267+
268+ // Add header information to evidence
269+ evidence += "\n\nHTTP Response Details:" ;
270+ evidence += "\nStatus Code: " + statusCode ;
271+ if ( contentType ) evidence += "\nContent-Type: " + contentType ;
272+ if ( mcpSessionId ) evidence += "\nMcp-Session-Id: " + mcpSessionId ;
273+ if ( transferEncoding ) evidence += "\nTransfer-Encoding: " + transferEncoding ;
274+ if ( server ) evidence += "\nServer: " + server ;
275+
276+ // Include response snippet in evidence (first 500 chars)
277+ if ( response . length > 0 ) {
278+ var responseSnippet = response . length > 500 ? response . substring ( 0 , 500 ) + "..." : response ;
279+ evidence += "\n\nResponse Body:\n" + responseSnippet ;
280+ } else if ( hasEventStream && hasMcpHeaders ) {
281+ evidence += "\n\nNote: SSE stream established - response body may be empty initially" ;
282+ } else {
283+ evidence += "\n\nNote: Response body was empty" ;
284+ }
285+
286+ raiseMcpAlert ( as , msg , evidence , confidence , risk , originalPayload ) ;
287+ return true ; // MCP server found
288+ }
289+
290+ return false ; // No MCP server detected
291+ }
292+
293+ /**
294+ * Raises an alert for detected MCP server
295+ * @param as - ActiveScan object
296+ * @param msg - HttpMessage
297+ * @param evidence - Evidence string
298+ * @param confidence - Confidence level (0-4)
299+ * @param risk - Risk level (0-3)
300+ * @param payload - Original payload sent
301+ */
302+ function raiseMcpAlert ( as , msg , evidence , confidence , risk , payload ) {
303+ print ( 'MCP Server Detector: Raising alert for ' + msg . getRequestHeader ( ) . getURI ( ) . toString ( ) ) ;
304+
305+ var alertTitle = "Open MCP Server Detected" ;
306+ var description = "A confirmed Model Context Protocol (MCP) server was detected through strict specification validation. " +
307+ "The server properly responds to MCP initialize requests with either: (1) Server-Sent Events format " +
308+ "(text/event-stream + Mcp-Session-Id header), or (2) Valid JSON format (application/json + proper MCP response structure). " +
309+ "MCP servers provide AI assistants with controlled access to tools and data sources. " +
310+ "If this server is unintentionally exposed, it could allow unauthorized access to internal tools, resources, or sensitive information." ;
311+
312+ var solution = "1. Verify if this MCP server should be publicly accessible\n" +
313+ "2. Implement proper authentication and authorization\n" +
314+ "3. Use network-level restrictions (firewall, VPN)\n" +
315+ "4. Regularly audit MCP server configurations\n" +
316+ "5. Monitor MCP server access logs" ;
317+
318+ var reference = "Model Context Protocol Specification: https://spec.modelcontextprotocol.io/specification/" ;
319+
320+ var otherInfo = "MCP servers support two response formats:\n" +
321+ "1. Server-Sent Events (text/event-stream) - for streaming responses\n" +
322+ "2. JSON (application/json) - for single JSON object responses\n\n" +
323+ "MCP servers typically expose methods like:\n" +
324+ "- initialize: Server initialization\n" +
325+ "- tools/list: Available tools\n" +
326+ "- resources/list: Available resources\n" +
327+ "- prompts/list: Available prompts\n\n" +
328+ "Original request payload:\n" + payload ;
329+
330+ as . newAlert ( )
331+ . setRisk ( risk )
332+ . setConfidence ( confidence )
333+ . setName ( alertTitle )
334+ . setDescription ( description )
335+ . setAttack ( payload )
336+ . setEvidence ( evidence )
337+ . setOtherInfo ( otherInfo )
338+ . setSolution ( solution )
339+ . setReference ( reference )
340+ . setCweId ( 200 )
341+ . setWascId ( 13 )
342+ . setMessage ( msg )
343+ . raise ( ) ;
344+ }
345+
346+ /**
347+ * Parameter-based scanning (not typically used for this type of detection)
348+ * @param as - ActiveScan object
349+ * @param msg - HttpMessage
350+ * @param param - Parameter name
351+ * @param value - Parameter value
352+ */
353+ function scan ( as , msg , param , value ) {
354+ // For MCP server detection, we focus on endpoint discovery rather than parameter manipulation
355+ // This function is included for completeness but not actively used
356+ return ;
357+ }
0 commit comments