Skip to content

Commit e0418ec

Browse files
authored
new active script to detect open mcp servers
1 parent 0e5e22b commit e0418ec

File tree

1 file changed

+357
-0
lines changed

1 file changed

+357
-0
lines changed

active/open_mcp.js

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
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

Comments
 (0)