@mcp-shark/mcp-shark 1.5.13 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +396 -59
- package/bin/mcp-shark.js +146 -52
- package/core/cli/AutoFixEngine.js +93 -0
- package/core/cli/ConfigScanner.js +193 -0
- package/core/cli/DataLoader.js +200 -0
- package/core/cli/DeclarativeRuleEngine.js +363 -0
- package/core/cli/DoctorCommand.js +218 -0
- package/core/cli/FixHandlers.js +222 -0
- package/core/cli/HtmlReportGenerator.js +203 -0
- package/core/cli/IdeConfigPaths.js +175 -0
- package/core/cli/ListCommand.js +224 -0
- package/core/cli/LockCommand.js +164 -0
- package/core/cli/LockDiffEngine.js +152 -0
- package/core/cli/RuleRegistryConfig.js +131 -0
- package/core/cli/ScanCommand.js +244 -0
- package/core/cli/ScanService.js +200 -0
- package/core/cli/SecretDetector.js +92 -0
- package/core/cli/SharkScoreCalculator.js +109 -0
- package/core/cli/ToolClassifications.js +51 -0
- package/core/cli/ToxicFlowAnalyzer.js +212 -0
- package/core/cli/UpdateCommand.js +188 -0
- package/core/cli/WalkthroughGenerator.js +195 -0
- package/core/cli/WatchCommand.js +129 -0
- package/core/cli/YamlRuleEngine.js +197 -0
- package/core/cli/data/rule-packs/agentic-security-2026.json +180 -0
- package/core/cli/data/rule-packs/general-security.json +173 -0
- package/core/cli/data/rule-packs/owasp-mcp-2026.json +244 -0
- package/core/cli/data/rule-packs/toxic-flow-heuristics.json +21 -0
- package/core/cli/data/rule-sources.json +5 -0
- package/core/cli/data/secret-patterns.json +18 -0
- package/core/cli/data/tool-classifications.json +111 -0
- package/core/cli/data/toxic-flow-rules.json +47 -0
- package/core/cli/index.js +23 -0
- package/core/cli/output/Banner.js +52 -0
- package/core/cli/output/Formatter.js +183 -0
- package/core/cli/output/JsonFormatter.js +106 -0
- package/core/cli/output/index.js +16 -0
- package/core/cli/secureRegistryFetch.js +157 -0
- package/core/cli/symbols.js +16 -0
- package/core/container/DependencyContainer.js +4 -1
- package/core/repositories/PacketRepository.js +16 -0
- package/core/services/AuditService.js +2 -0
- package/core/services/security/StaticRulesService.js +69 -13
- package/core/services/security/TrafficAnalysisService.js +19 -1
- package/core/services/security/TrafficToxicFlowService.js +154 -0
- package/core/services/security/index.js +2 -1
- package/core/services/security/rules/index.js +25 -59
- package/core/services/security/rules/scans/configPermissions.js +91 -0
- package/core/services/security/rules/scans/duplicateToolNames.js +85 -0
- package/core/services/security/rules/scans/insecureTransport.js +148 -0
- package/core/services/security/rules/scans/missingContainment.js +123 -0
- package/core/services/security/rules/scans/shellEnvInjection.js +101 -0
- package/core/services/security/rules/scans/unsafeDefaults.js +99 -0
- package/core/services/security/toolsListFromTrafficParser.js +70 -0
- package/core/tui/App.js +144 -0
- package/core/tui/FindingsPanel.js +115 -0
- package/core/tui/FixPanel.js +132 -0
- package/core/tui/Header.js +51 -0
- package/core/tui/HelpBar.js +42 -0
- package/core/tui/ServersPanel.js +109 -0
- package/core/tui/ToxicFlowsPanel.js +100 -0
- package/core/tui/h.js +8 -0
- package/core/tui/index.js +11 -0
- package/core/tui/render.js +22 -0
- package/package.json +24 -16
- package/ui/dist/assets/{index-CiCSDYf-.js → index-Buah4fNS.js} +32 -32
- package/ui/dist/index.html +1 -1
- package/ui/server/controllers/RequestController.js +9 -1
- package/ui/server/controllers/SecurityFindingsController.js +46 -1
- package/ui/server/routes/requests.js +8 -1
- package/ui/server/routes/security.js +5 -1
- package/ui/server/setup.js +2 -0
- package/ui/server/swagger/paths/components.js +55 -0
- package/ui/server/swagger/paths/securityTrafficFlows.js +59 -0
- package/ui/server/swagger/paths.js +2 -0
- package/ui/server/swagger/swagger.js +5 -1
- package/ui/src/Security.jsx +10 -0
- package/ui/src/components/Security/ScannerContent.jsx +30 -1
- package/ui/src/components/Security/TrafficToxicFlowsPanel.jsx +253 -0
- package/ui/src/components/Security/securityApi.js +15 -0
- package/ui/src/components/Security/useSecurity.js +60 -3
- package/core/services/security/rules/scans/agentic01GoalHijack.js +0 -130
- package/core/services/security/rules/scans/agentic02ToolMisuse.js +0 -129
- package/core/services/security/rules/scans/agentic03IdentityAbuse.js +0 -130
- package/core/services/security/rules/scans/agentic04SupplyChain.js +0 -130
- package/core/services/security/rules/scans/agentic06MemoryPoisoning.js +0 -130
- package/core/services/security/rules/scans/agentic07InsecureCommunication.js +0 -135
- package/core/services/security/rules/scans/agentic08CascadingFailures.js +0 -135
- package/core/services/security/rules/scans/agentic09TrustExploitation.js +0 -135
- package/core/services/security/rules/scans/agentic10RogueAgent.js +0 -130
- package/core/services/security/rules/scans/hardcodedSecrets.js +0 -130
- package/core/services/security/rules/scans/mcp01TokenMismanagement.js +0 -127
- package/core/services/security/rules/scans/mcp02ScopeCreep.js +0 -130
- package/core/services/security/rules/scans/mcp03ToolPoisoning.js +0 -132
- package/core/services/security/rules/scans/mcp04SupplyChain.js +0 -131
- package/core/services/security/rules/scans/mcp06PromptInjection.js +0 -200
- package/core/services/security/rules/scans/mcp07InsufficientAuth.js +0 -130
- package/core/services/security/rules/scans/mcp08LackAudit.js +0 -129
- package/core/services/security/rules/scans/mcp09ShadowServers.js +0 -129
- package/core/services/security/rules/scans/mcp10ContextInjection.js +0 -130
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate Tool Names Detection (Config-Level)
|
|
3
|
+
* Detects tools with identical names across different servers,
|
|
4
|
+
* which enables cross-server shadowing attacks.
|
|
5
|
+
* Catalog reference: §1.4 (tool name collision)
|
|
6
|
+
*/
|
|
7
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
8
|
+
|
|
9
|
+
const RULE_ID = 'duplicate-tool-names';
|
|
10
|
+
const OWASP_ID = 'MCP09';
|
|
11
|
+
const RECOMMENDATION =
|
|
12
|
+
'Rename tools to be unique across all servers, or isolate conflicting servers into separate agent sessions.';
|
|
13
|
+
|
|
14
|
+
export function scanDuplicateToolNames(_mcpData = {}) {
|
|
15
|
+
return {
|
|
16
|
+
toolFindings: [],
|
|
17
|
+
resourceFindings: [],
|
|
18
|
+
promptFindings: [],
|
|
19
|
+
notablePatterns: [],
|
|
20
|
+
recommendations: [RECOMMENDATION],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Analyze all servers for duplicate tool names
|
|
26
|
+
* Called directly by ScanService with the full server list
|
|
27
|
+
* @param {Array} servers - Flat array of server objects
|
|
28
|
+
* @returns {Array} Findings for duplicate tool names
|
|
29
|
+
*/
|
|
30
|
+
export function analyzeAllServerToolNames(servers) {
|
|
31
|
+
const toolMap = new Map();
|
|
32
|
+
|
|
33
|
+
for (const server of servers) {
|
|
34
|
+
const tools = Array.isArray(server.tools) ? server.tools : [];
|
|
35
|
+
for (const tool of tools) {
|
|
36
|
+
const name = typeof tool === 'string' ? tool : tool?.name;
|
|
37
|
+
if (!name) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!toolMap.has(name)) {
|
|
41
|
+
toolMap.set(name, []);
|
|
42
|
+
}
|
|
43
|
+
toolMap.get(name).push(server.name);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const findings = [];
|
|
48
|
+
for (const [toolName, serverNames] of toolMap) {
|
|
49
|
+
if (serverNames.length <= 1) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const uniqueServers = [...new Set(serverNames)];
|
|
53
|
+
if (uniqueServers.length <= 1) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
findings.push({
|
|
58
|
+
rule_id: RULE_ID,
|
|
59
|
+
severity: 'high',
|
|
60
|
+
owasp_id: OWASP_ID,
|
|
61
|
+
title: `Duplicate tool "${toolName}" across ${uniqueServers.length} servers`,
|
|
62
|
+
description: `Tool "${toolName}" exists in ${uniqueServers.join(', ')}. An attacker could shadow one server's tool with another.`,
|
|
63
|
+
recommendation: RECOMMENDATION,
|
|
64
|
+
confidence: 'definite',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const adapter = createRuleAdapter(scanDuplicateToolNames, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
72
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
73
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
74
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
75
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
76
|
+
|
|
77
|
+
export const ruleMetadata = {
|
|
78
|
+
id: RULE_ID,
|
|
79
|
+
name: 'Duplicate Tool Names',
|
|
80
|
+
owasp_id: OWASP_ID,
|
|
81
|
+
severity: 'high',
|
|
82
|
+
description: 'Detects identical tool names across servers enabling shadowing attacks.',
|
|
83
|
+
source: 'static',
|
|
84
|
+
type: 'general-security',
|
|
85
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Insecure Transport Detection
|
|
3
|
+
* Detects MCP servers using HTTP instead of HTTPS, or SSE/WebSocket
|
|
4
|
+
* connections without TLS, exposing traffic to interception.
|
|
5
|
+
* Catalog reference: §1.7
|
|
6
|
+
*/
|
|
7
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
8
|
+
import { toolToText } from '../utils/text.js';
|
|
9
|
+
|
|
10
|
+
const RULE_ID = 'insecure-transport';
|
|
11
|
+
const OWASP_ID = 'MCP07';
|
|
12
|
+
const RECOMMENDATION =
|
|
13
|
+
'Use HTTPS/WSS for all remote MCP server connections. localhost HTTP is acceptable for local-only servers.';
|
|
14
|
+
|
|
15
|
+
const HTTP_URL_IN_TEXT = /http:\/\/[^\s'"<>]+/gi;
|
|
16
|
+
const WS_URL_IN_TEXT = /ws:\/\/[^\s'"<>]+/gi;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* True when URL host is loopback / local bind only (exact host match, not localhost.evil.com).
|
|
20
|
+
*/
|
|
21
|
+
function isLocalhostUrl(urlString) {
|
|
22
|
+
try {
|
|
23
|
+
const u = new URL(urlString);
|
|
24
|
+
const h = u.hostname.toLowerCase();
|
|
25
|
+
return (
|
|
26
|
+
h === 'localhost' || h === '127.0.0.1' || h === '0.0.0.0' || h === '[::1]' || h === '::1'
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function textHasNonLocalHttp(text) {
|
|
34
|
+
for (const m of text.matchAll(HTTP_URL_IN_TEXT)) {
|
|
35
|
+
if (!isLocalhostUrl(m[0])) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function textHasNonLocalWs(text) {
|
|
43
|
+
for (const m of text.matchAll(WS_URL_IN_TEXT)) {
|
|
44
|
+
if (!isLocalhostUrl(m[0])) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function scanInsecureTransport(mcpData = {}) {
|
|
52
|
+
const results = {
|
|
53
|
+
toolFindings: [],
|
|
54
|
+
resourceFindings: [],
|
|
55
|
+
promptFindings: [],
|
|
56
|
+
notablePatterns: [],
|
|
57
|
+
recommendations: [RECOMMENDATION],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const tool of mcpData.tools || []) {
|
|
61
|
+
const text = toolToText(tool);
|
|
62
|
+
|
|
63
|
+
if (textHasNonLocalHttp(text)) {
|
|
64
|
+
results.toolFindings.push({
|
|
65
|
+
issueType: 'Insecure Transport (HTTP)',
|
|
66
|
+
name: tool?.name || 'tool',
|
|
67
|
+
severity: 'medium',
|
|
68
|
+
reasons: [
|
|
69
|
+
`Tool "${tool?.name || 'unknown'}" references a non-localhost HTTP URL. Traffic can be intercepted.`,
|
|
70
|
+
],
|
|
71
|
+
tags: ['transport', 'tls'],
|
|
72
|
+
safeUseNotes: RECOMMENDATION,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (textHasNonLocalWs(text)) {
|
|
77
|
+
results.toolFindings.push({
|
|
78
|
+
issueType: 'Insecure Transport (WebSocket)',
|
|
79
|
+
name: tool?.name || 'tool',
|
|
80
|
+
severity: 'medium',
|
|
81
|
+
reasons: [
|
|
82
|
+
`Tool "${tool?.name || 'unknown'}" references a non-localhost WS URL. Traffic can be intercepted.`,
|
|
83
|
+
],
|
|
84
|
+
tags: ['transport', 'websocket'],
|
|
85
|
+
safeUseNotes: RECOMMENDATION,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Analyze a server config for insecure transport
|
|
95
|
+
* Called directly by ScanService
|
|
96
|
+
*/
|
|
97
|
+
export function analyzeServerTransport(serverName, config) {
|
|
98
|
+
if (!config?.url) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const findings = [];
|
|
103
|
+
const urlStr = config.url;
|
|
104
|
+
|
|
105
|
+
if (urlStr.startsWith('http://') && !isLocalhostUrl(urlStr)) {
|
|
106
|
+
findings.push({
|
|
107
|
+
rule_id: RULE_ID,
|
|
108
|
+
severity: 'high',
|
|
109
|
+
owasp_id: OWASP_ID,
|
|
110
|
+
title: `Insecure transport: ${serverName} uses HTTP`,
|
|
111
|
+
description: `${serverName} connects to ${config.url} over plain HTTP. Credentials and data are transmitted in cleartext.`,
|
|
112
|
+
recommendation: RECOMMENDATION,
|
|
113
|
+
server_name: serverName,
|
|
114
|
+
confidence: 'definite',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (urlStr.startsWith('ws://') && !isLocalhostUrl(urlStr)) {
|
|
119
|
+
findings.push({
|
|
120
|
+
rule_id: RULE_ID,
|
|
121
|
+
severity: 'high',
|
|
122
|
+
owasp_id: OWASP_ID,
|
|
123
|
+
title: `Insecure transport: ${serverName} uses WS`,
|
|
124
|
+
description: `${serverName} connects to ${config.url} over plain WebSocket. Upgrade to WSS.`,
|
|
125
|
+
recommendation: RECOMMENDATION,
|
|
126
|
+
server_name: serverName,
|
|
127
|
+
confidence: 'definite',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const adapter = createRuleAdapter(scanInsecureTransport, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
135
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
136
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
137
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
138
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
139
|
+
|
|
140
|
+
export const ruleMetadata = {
|
|
141
|
+
id: RULE_ID,
|
|
142
|
+
name: 'Insecure Transport',
|
|
143
|
+
owasp_id: OWASP_ID,
|
|
144
|
+
severity: 'medium',
|
|
145
|
+
description: 'Detects non-localhost HTTP/WS connections that expose traffic to interception.',
|
|
146
|
+
source: 'static',
|
|
147
|
+
type: 'general-security',
|
|
148
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Missing Directory Containment Detection
|
|
3
|
+
* Detects filesystem-type MCP servers without directory restrictions,
|
|
4
|
+
* granting full filesystem access to the agent.
|
|
5
|
+
* Catalog reference: §1.1 (unrestricted file access)
|
|
6
|
+
*/
|
|
7
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
8
|
+
import { toolToText } from '../utils/text.js';
|
|
9
|
+
|
|
10
|
+
const RULE_ID = 'missing-containment';
|
|
11
|
+
const OWASP_ID = 'MCP10';
|
|
12
|
+
const RECOMMENDATION =
|
|
13
|
+
'Restrict filesystem servers to specific directories using allowlist args (e.g., --allow-dir /path/to/project).';
|
|
14
|
+
|
|
15
|
+
const FS_SERVER_PATTERNS = [
|
|
16
|
+
/filesystem/i,
|
|
17
|
+
/file.?system/i,
|
|
18
|
+
/fs.?server/i,
|
|
19
|
+
/read_file|write_file|list_directory/i,
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const CONTAINMENT_INDICATORS = [
|
|
23
|
+
/--allow/i,
|
|
24
|
+
/--root/i,
|
|
25
|
+
/--sandbox/i,
|
|
26
|
+
/allowedDirectories/i,
|
|
27
|
+
/rootPath/i,
|
|
28
|
+
/basePath/i,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function isFilesystemServer(serverName, config) {
|
|
32
|
+
const text = `${serverName} ${JSON.stringify(config || {})}`;
|
|
33
|
+
for (const pattern of FS_SERVER_PATTERNS) {
|
|
34
|
+
if (pattern.test(text)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasContainment(config) {
|
|
42
|
+
const text = JSON.stringify(config || {});
|
|
43
|
+
for (const pattern of CONTAINMENT_INDICATORS) {
|
|
44
|
+
if (pattern.test(text)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function scanMissingContainment(mcpData = {}) {
|
|
52
|
+
const results = {
|
|
53
|
+
toolFindings: [],
|
|
54
|
+
resourceFindings: [],
|
|
55
|
+
promptFindings: [],
|
|
56
|
+
notablePatterns: [],
|
|
57
|
+
recommendations: [RECOMMENDATION],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const tool of mcpData.tools || []) {
|
|
61
|
+
const text = toolToText(tool);
|
|
62
|
+
const hasFilesystem = FS_SERVER_PATTERNS.some((p) => p.test(text));
|
|
63
|
+
if (hasFilesystem) {
|
|
64
|
+
const contained = CONTAINMENT_INDICATORS.some((p) => p.test(text));
|
|
65
|
+
if (!contained) {
|
|
66
|
+
results.toolFindings.push({
|
|
67
|
+
issueType: 'Missing Directory Containment',
|
|
68
|
+
name: tool?.name || 'tool',
|
|
69
|
+
severity: 'medium',
|
|
70
|
+
reasons: [
|
|
71
|
+
`Tool "${tool?.name || 'unknown'}" has filesystem access without directory containment.`,
|
|
72
|
+
],
|
|
73
|
+
tags: ['containment', 'filesystem'],
|
|
74
|
+
safeUseNotes: RECOMMENDATION,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Analyze a server config for missing containment
|
|
85
|
+
* Called directly by ScanService
|
|
86
|
+
*/
|
|
87
|
+
export function analyzeServerContainment(serverName, config) {
|
|
88
|
+
if (!isFilesystemServer(serverName, config)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
if (hasContainment(config)) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
rule_id: RULE_ID,
|
|
98
|
+
severity: 'medium',
|
|
99
|
+
owasp_id: OWASP_ID,
|
|
100
|
+
title: `No directory containment on ${serverName}`,
|
|
101
|
+
description: `${serverName} has filesystem access without directory restrictions. The agent can read/write any file.`,
|
|
102
|
+
recommendation: RECOMMENDATION,
|
|
103
|
+
server_name: serverName,
|
|
104
|
+
confidence: 'possible',
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const adapter = createRuleAdapter(scanMissingContainment, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
110
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
111
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
112
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
113
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
114
|
+
|
|
115
|
+
export const ruleMetadata = {
|
|
116
|
+
id: RULE_ID,
|
|
117
|
+
name: 'Missing Directory Containment',
|
|
118
|
+
owasp_id: OWASP_ID,
|
|
119
|
+
severity: 'medium',
|
|
120
|
+
description: 'Detects filesystem servers without directory restrictions.',
|
|
121
|
+
source: 'static',
|
|
122
|
+
type: 'general-security',
|
|
123
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell/Environment Injection Detection
|
|
3
|
+
* Detects MCP server configs using shell: true or unsanitized
|
|
4
|
+
* environment variable interpolation in commands, which can
|
|
5
|
+
* enable command injection via environment variables.
|
|
6
|
+
* Catalog reference: §1.9 (env-based command injection)
|
|
7
|
+
*/
|
|
8
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
9
|
+
|
|
10
|
+
const RULE_ID = 'shell-env-injection';
|
|
11
|
+
const OWASP_ID = 'MCP05';
|
|
12
|
+
const RECOMMENDATION =
|
|
13
|
+
'Avoid shell: true in server spawn configs. Use direct command arrays instead of shell string interpolation.';
|
|
14
|
+
|
|
15
|
+
export function scanShellEnvInjection(_mcpData = {}) {
|
|
16
|
+
return {
|
|
17
|
+
toolFindings: [],
|
|
18
|
+
resourceFindings: [],
|
|
19
|
+
promptFindings: [],
|
|
20
|
+
notablePatterns: [],
|
|
21
|
+
recommendations: [RECOMMENDATION],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Analyze a server config for shell injection risks
|
|
27
|
+
* Called directly by ScanService
|
|
28
|
+
* @param {string} serverName
|
|
29
|
+
* @param {object} config - Server config with command, args, env
|
|
30
|
+
* @returns {Array} Findings
|
|
31
|
+
*/
|
|
32
|
+
export function analyzeServerShellRisk(serverName, config) {
|
|
33
|
+
const findings = [];
|
|
34
|
+
|
|
35
|
+
if (!config) {
|
|
36
|
+
return findings;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const command = config.command || '';
|
|
40
|
+
const args = config.args || [];
|
|
41
|
+
const argsStr = Array.isArray(args) ? args.join(' ') : String(args);
|
|
42
|
+
const fullCommand = `${command} ${argsStr}`;
|
|
43
|
+
|
|
44
|
+
if (hasShellInterpolation(fullCommand)) {
|
|
45
|
+
findings.push({
|
|
46
|
+
rule_id: RULE_ID,
|
|
47
|
+
severity: 'high',
|
|
48
|
+
owasp_id: OWASP_ID,
|
|
49
|
+
title: `Shell interpolation in ${serverName} command`,
|
|
50
|
+
description: `Server "${serverName}" command contains shell variable interpolation that could be exploited if env vars are attacker-controlled.`,
|
|
51
|
+
recommendation: RECOMMENDATION,
|
|
52
|
+
server_name: serverName,
|
|
53
|
+
confidence: 'probable',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (hasPipeOrChain(fullCommand)) {
|
|
58
|
+
findings.push({
|
|
59
|
+
rule_id: RULE_ID,
|
|
60
|
+
severity: 'critical',
|
|
61
|
+
owasp_id: OWASP_ID,
|
|
62
|
+
title: `Shell pipe/chain in ${serverName} command`,
|
|
63
|
+
description: `Server "${serverName}" command uses shell pipes or chains (|, &&, ;) which are dangerous with shell: true.`,
|
|
64
|
+
recommendation: 'Remove shell operators from server command. Use direct process spawning.',
|
|
65
|
+
server_name: serverName,
|
|
66
|
+
confidence: 'definite',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return findings;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check for shell variable interpolation patterns
|
|
75
|
+
*/
|
|
76
|
+
function hasShellInterpolation(text) {
|
|
77
|
+
return /\$\([^)]+\)/.test(text) || /`[^`]+`/.test(text);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check for shell pipe or chain operators
|
|
82
|
+
*/
|
|
83
|
+
function hasPipeOrChain(text) {
|
|
84
|
+
return /[|&;]/.test(text) && !/https?:\/\//.test(text);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const adapter = createRuleAdapter(scanShellEnvInjection, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
88
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
89
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
90
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
91
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
92
|
+
|
|
93
|
+
export const ruleMetadata = {
|
|
94
|
+
id: RULE_ID,
|
|
95
|
+
name: 'Shell/Environment Injection',
|
|
96
|
+
owasp_id: OWASP_ID,
|
|
97
|
+
severity: 'high',
|
|
98
|
+
description: 'Detects shell interpolation and pipe operators in server commands.',
|
|
99
|
+
source: 'static',
|
|
100
|
+
type: 'general-security',
|
|
101
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unsafe Default Configuration Detection
|
|
3
|
+
* Detects MCP servers running with insecure default settings
|
|
4
|
+
* such as debug mode, verbose logging of secrets, or disabled auth.
|
|
5
|
+
*/
|
|
6
|
+
import { createRuleAdapter } from '../utils/adapter.js';
|
|
7
|
+
import { toolToText } from '../utils/text.js';
|
|
8
|
+
|
|
9
|
+
const RULE_ID = 'unsafe-defaults';
|
|
10
|
+
const OWASP_ID = 'MCP07';
|
|
11
|
+
const RECOMMENDATION =
|
|
12
|
+
'Disable debug mode and verbose logging in production. Enable authentication on all server endpoints.';
|
|
13
|
+
|
|
14
|
+
const UNSAFE_DEFAULT_PATTERNS = [
|
|
15
|
+
{ pattern: /debug[=:]\s*true/i, label: 'debug mode enabled' },
|
|
16
|
+
{ pattern: /--debug\b/i, label: 'debug flag' },
|
|
17
|
+
{ pattern: /NODE_ENV[=:]\s*development/i, label: 'NODE_ENV=development' },
|
|
18
|
+
{ pattern: /verbose[=:]\s*true/i, label: 'verbose logging enabled' },
|
|
19
|
+
{ pattern: /log[_-]?level[=:]\s*(debug|trace)/i, label: 'debug-level logging' },
|
|
20
|
+
{ pattern: /auth[=:]\s*false/i, label: 'authentication disabled' },
|
|
21
|
+
{ pattern: /--no-auth\b/i, label: 'no-auth flag' },
|
|
22
|
+
{ pattern: /disable[_-]?auth/i, label: 'auth disabled' },
|
|
23
|
+
{ pattern: /tls[=:]\s*false/i, label: 'TLS disabled' },
|
|
24
|
+
{ pattern: /--insecure\b/i, label: 'insecure flag' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function scanUnsafeDefaults(mcpData = {}) {
|
|
28
|
+
const results = {
|
|
29
|
+
toolFindings: [],
|
|
30
|
+
resourceFindings: [],
|
|
31
|
+
promptFindings: [],
|
|
32
|
+
notablePatterns: [],
|
|
33
|
+
recommendations: [RECOMMENDATION],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (const tool of mcpData.tools || []) {
|
|
37
|
+
const text = toolToText(tool);
|
|
38
|
+
for (const { pattern, label } of UNSAFE_DEFAULT_PATTERNS) {
|
|
39
|
+
if (pattern.test(text)) {
|
|
40
|
+
results.toolFindings.push({
|
|
41
|
+
issueType: 'Unsafe Default Configuration',
|
|
42
|
+
name: tool?.name || 'tool',
|
|
43
|
+
severity: 'medium',
|
|
44
|
+
reasons: [
|
|
45
|
+
`Tool "${tool?.name || 'unknown'}" has ${label} — this is insecure for production use.`,
|
|
46
|
+
],
|
|
47
|
+
tags: ['defaults', 'configuration'],
|
|
48
|
+
safeUseNotes: RECOMMENDATION,
|
|
49
|
+
});
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return results;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Analyze a server config for unsafe defaults
|
|
60
|
+
* Called directly by ScanService
|
|
61
|
+
*/
|
|
62
|
+
export function analyzeServerDefaults(serverName, config) {
|
|
63
|
+
const text = JSON.stringify(config || {});
|
|
64
|
+
const findings = [];
|
|
65
|
+
|
|
66
|
+
for (const { pattern, label } of UNSAFE_DEFAULT_PATTERNS) {
|
|
67
|
+
if (pattern.test(text)) {
|
|
68
|
+
findings.push({
|
|
69
|
+
rule_id: RULE_ID,
|
|
70
|
+
severity: 'medium',
|
|
71
|
+
owasp_id: OWASP_ID,
|
|
72
|
+
title: `Unsafe default: ${label} on ${serverName}`,
|
|
73
|
+
description: `${serverName} has ${label}. This may expose sensitive data or weaken security.`,
|
|
74
|
+
recommendation: RECOMMENDATION,
|
|
75
|
+
server_name: serverName,
|
|
76
|
+
confidence: 'probable',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return findings;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const adapter = createRuleAdapter(scanUnsafeDefaults, RULE_ID, OWASP_ID, RECOMMENDATION);
|
|
85
|
+
export const analyzeTool = adapter.analyzeTool;
|
|
86
|
+
export const analyzePrompt = adapter.analyzePrompt;
|
|
87
|
+
export const analyzeResource = adapter.analyzeResource;
|
|
88
|
+
export const analyzePacket = adapter.analyzePacket;
|
|
89
|
+
|
|
90
|
+
export const ruleMetadata = {
|
|
91
|
+
id: RULE_ID,
|
|
92
|
+
name: 'Unsafe Default Configuration',
|
|
93
|
+
owasp_id: OWASP_ID,
|
|
94
|
+
severity: 'medium',
|
|
95
|
+
description:
|
|
96
|
+
'Detects servers running with insecure default settings like debug mode or disabled auth.',
|
|
97
|
+
source: 'static',
|
|
98
|
+
type: 'general-security',
|
|
99
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract MCP tools/list (or equivalent) tool arrays from proxied JSON-RPC traffic.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function normalizeTools(tools) {
|
|
6
|
+
if (!Array.isArray(tools)) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const out = [];
|
|
10
|
+
for (const t of tools) {
|
|
11
|
+
if (!t || typeof t !== 'object') {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const name = t.name;
|
|
15
|
+
if (typeof name !== 'string' || !name) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
out.push({
|
|
19
|
+
name,
|
|
20
|
+
description: typeof t.description === 'string' ? t.description : null,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return out.length > 0 ? out : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseJson(value) {
|
|
27
|
+
if (value == null) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === 'object') {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== 'string' || value === '') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(value);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse stored jsonrpc_result column (stringified `result` object).
|
|
45
|
+
* @param {string|null} jsonrpcResultStr
|
|
46
|
+
* @returns {Array<{name: string, description: string|null}>|null}
|
|
47
|
+
*/
|
|
48
|
+
export function toolsFromJsonrpcResultString(jsonrpcResultStr) {
|
|
49
|
+
const obj = parseJson(jsonrpcResultStr);
|
|
50
|
+
if (!obj || !Array.isArray(obj.tools)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return normalizeTools(obj.tools);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse live or raw response body for tools/list success.
|
|
58
|
+
* @param {string|object|null|undefined} body
|
|
59
|
+
* @returns {Array<{name: string, description: string|null}>|null}
|
|
60
|
+
*/
|
|
61
|
+
export function toolsFromTrafficResponseBody(body) {
|
|
62
|
+
const parsed = parseJson(body);
|
|
63
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (parsed.result && Array.isArray(parsed.result.tools)) {
|
|
67
|
+
return normalizeTools(parsed.result.tools);
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|