@bryan-thompson/inspector-assessment-client 1.26.6 → 1.26.7
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/lib/lib/assessment/configTypes.d.ts +2 -0
- package/lib/lib/assessment/configTypes.d.ts.map +1 -1
- package/lib/lib/securityPatterns.d.ts +4 -2
- package/lib/lib/securityPatterns.d.ts.map +1 -1
- package/lib/lib/securityPatterns.js +146 -2
- package/lib/services/assessment/modules/AUPComplianceAssessor.js +9 -9
- package/lib/services/assessment/modules/AuthenticationAssessor.js +4 -4
- package/lib/services/assessment/modules/BaseAssessor.d.ts +0 -14
- package/lib/services/assessment/modules/BaseAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/BaseAssessor.js +1 -33
- package/lib/services/assessment/modules/CrossCapabilitySecurityAssessor.js +1 -1
- package/lib/services/assessment/modules/DeveloperExperienceAssessor.js +1 -1
- package/lib/services/assessment/modules/DocumentationAssessor.js +2 -2
- package/lib/services/assessment/modules/ErrorHandlingAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/ErrorHandlingAssessor.js +8 -8
- package/lib/services/assessment/modules/ExternalAPIScannerAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/ExternalAPIScannerAssessor.js +3 -3
- package/lib/services/assessment/modules/FunctionalityAssessor.js +9 -9
- package/lib/services/assessment/modules/MCPSpecComplianceAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/MCPSpecComplianceAssessor.js +12 -12
- package/lib/services/assessment/modules/ManifestValidationAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/ManifestValidationAssessor.js +9 -5
- package/lib/services/assessment/modules/PortabilityAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/PortabilityAssessor.js +3 -3
- package/lib/services/assessment/modules/ProhibitedLibrariesAssessor.js +4 -4
- package/lib/services/assessment/modules/PromptAssessor.js +2 -2
- package/lib/services/assessment/modules/ProtocolComplianceAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/ProtocolComplianceAssessor.js +7 -7
- package/lib/services/assessment/modules/ProtocolConformanceAssessor.js +1 -1
- package/lib/services/assessment/modules/ResourceAssessor.js +1 -1
- package/lib/services/assessment/modules/SecurityAssessor.d.ts +25 -2
- package/lib/services/assessment/modules/SecurityAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/SecurityAssessor.js +149 -17
- package/lib/services/assessment/modules/TemporalAssessor.d.ts.map +1 -1
- package/lib/services/assessment/modules/TemporalAssessor.js +10 -10
- package/lib/services/assessment/modules/ToolAnnotationAssessor.js +9 -9
- package/lib/services/assessment/modules/UsabilityAssessor.js +1 -1
- package/lib/services/assessment/modules/annotations/DescriptionPoisoningDetector.d.ts.map +1 -1
- package/lib/services/assessment/modules/annotations/DescriptionPoisoningDetector.js +37 -0
- package/lib/services/assessment/modules/index.d.ts +3 -0
- package/lib/services/assessment/modules/index.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/ChainExecutionTester.d.ts +104 -0
- package/lib/services/assessment/modules/securityTests/ChainExecutionTester.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ChainExecutionTester.js +257 -0
- package/lib/services/assessment/modules/securityTests/CrossToolStateTester.d.ts +91 -0
- package/lib/services/assessment/modules/securityTests/CrossToolStateTester.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/CrossToolStateTester.js +225 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts +120 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.js +338 -0
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts +59 -0
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.js +168 -0
- package/lib/services/assessment/modules/securityTests/index.d.ts +3 -1
- package/lib/services/assessment/modules/securityTests/index.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/index.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain Execution Tester
|
|
3
|
+
* Dynamic testing for multi-tool chain exploitation vulnerabilities
|
|
4
|
+
*
|
|
5
|
+
* Issue #93, Challenge #6: Multi-tool chained exploitation attacks
|
|
6
|
+
* Tests for:
|
|
7
|
+
* 1. Arbitrary tool invocation without allowlist
|
|
8
|
+
* 2. Output injection via {{output}} template substitution
|
|
9
|
+
* 3. Recursive chain execution (DoS potential)
|
|
10
|
+
* 4. State poisoning between chain steps
|
|
11
|
+
* 5. Missing depth/size limits
|
|
12
|
+
*
|
|
13
|
+
* A/B Validation:
|
|
14
|
+
* - vulnerable-mcp (10900): Should detect all vulnerability categories
|
|
15
|
+
* - hardened-mcp (10901): 0 false positives (validation-only behavior)
|
|
16
|
+
*/
|
|
17
|
+
import { SecurityResponseAnalyzer, } from "./SecurityResponseAnalyzer.js";
|
|
18
|
+
/**
|
|
19
|
+
* Tests for multi-tool chain exploitation vulnerabilities
|
|
20
|
+
*/
|
|
21
|
+
export class ChainExecutionTester {
|
|
22
|
+
verbose;
|
|
23
|
+
analyzer;
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
this.verbose = config.verbose ?? false;
|
|
26
|
+
this.analyzer = new SecurityResponseAnalyzer();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Log message if verbose logging is enabled
|
|
30
|
+
*/
|
|
31
|
+
log(message) {
|
|
32
|
+
if (this.verbose) {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log(`[ChainExecutionTester] ${message}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Identify tools that might be chain executors
|
|
39
|
+
* Looks for tools with names/descriptions/parameters suggesting chain execution
|
|
40
|
+
*/
|
|
41
|
+
identifyChainExecutorTools(tools) {
|
|
42
|
+
const chainNamePatterns = [
|
|
43
|
+
/chain/i,
|
|
44
|
+
/executor/i,
|
|
45
|
+
/pipeline/i,
|
|
46
|
+
/sequence/i,
|
|
47
|
+
/workflow/i,
|
|
48
|
+
/orchestrat/i,
|
|
49
|
+
/multi.*tool/i,
|
|
50
|
+
/batch/i,
|
|
51
|
+
];
|
|
52
|
+
const chainParamPatterns = [
|
|
53
|
+
/chain/i,
|
|
54
|
+
/steps/i,
|
|
55
|
+
/sequence/i,
|
|
56
|
+
/pipeline/i,
|
|
57
|
+
/tools/i,
|
|
58
|
+
/commands/i,
|
|
59
|
+
];
|
|
60
|
+
return tools.filter((tool) => {
|
|
61
|
+
// Check tool name
|
|
62
|
+
const nameMatches = chainNamePatterns.some((p) => p.test(tool.name));
|
|
63
|
+
// Check description
|
|
64
|
+
const descMatches = tool.description &&
|
|
65
|
+
chainNamePatterns.some((p) => p.test(tool.description || ""));
|
|
66
|
+
// Check parameter names
|
|
67
|
+
const schema = tool.inputSchema;
|
|
68
|
+
const paramNames = Object.keys(schema?.properties || {});
|
|
69
|
+
const paramMatches = paramNames.some((param) => chainParamPatterns.some((p) => p.test(param)));
|
|
70
|
+
return nameMatches || descMatches || paramMatches;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the parameter name for chain input from tool schema
|
|
75
|
+
*/
|
|
76
|
+
getChainParamName(tool) {
|
|
77
|
+
const schema = tool.inputSchema;
|
|
78
|
+
const paramNames = Object.keys(schema?.properties || {});
|
|
79
|
+
// Look for chain-like parameter names
|
|
80
|
+
const chainParam = paramNames.find((p) => /chain|steps|sequence|pipeline/i.test(p));
|
|
81
|
+
return chainParam || "chain";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract text content from tool response
|
|
85
|
+
*/
|
|
86
|
+
extractResponseText(response) {
|
|
87
|
+
if (!response)
|
|
88
|
+
return "";
|
|
89
|
+
// Handle content array format
|
|
90
|
+
if (response.content && Array.isArray(response.content)) {
|
|
91
|
+
return response.content
|
|
92
|
+
.map((item) => {
|
|
93
|
+
if (typeof item === "string")
|
|
94
|
+
return item;
|
|
95
|
+
if (item && typeof item === "object" && "text" in item)
|
|
96
|
+
return String(item.text);
|
|
97
|
+
return JSON.stringify(item);
|
|
98
|
+
})
|
|
99
|
+
.join("\n");
|
|
100
|
+
}
|
|
101
|
+
return "";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Determine the vulnerability reason from analysis result
|
|
105
|
+
*/
|
|
106
|
+
determineVulnerabilityReason(analysis) {
|
|
107
|
+
if (analysis.vulnerabilityCategories.includes("OUTPUT_INJECTION")) {
|
|
108
|
+
return "output_injection_detected";
|
|
109
|
+
}
|
|
110
|
+
if (analysis.vulnerabilityCategories.includes("RECURSIVE_CHAIN")) {
|
|
111
|
+
return "recursive_execution_detected";
|
|
112
|
+
}
|
|
113
|
+
if (analysis.vulnerabilityCategories.includes("ARBITRARY_TOOL_INVOCATION")) {
|
|
114
|
+
return "arbitrary_tool_accepted";
|
|
115
|
+
}
|
|
116
|
+
if (analysis.vulnerabilityCategories.includes("STATE_POISONING")) {
|
|
117
|
+
return "state_poisoning_detected";
|
|
118
|
+
}
|
|
119
|
+
return "chain_execution_confirmed";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Test single chain payload against a tool
|
|
123
|
+
*/
|
|
124
|
+
async testChainPayload(callTool, tool, chainPayload, paramName) {
|
|
125
|
+
const actualParamName = paramName || this.getChainParamName(tool);
|
|
126
|
+
try {
|
|
127
|
+
this.log(`Testing payload on ${tool.name} with param ${actualParamName}`);
|
|
128
|
+
const response = await callTool(tool.name, {
|
|
129
|
+
[actualParamName]: chainPayload,
|
|
130
|
+
});
|
|
131
|
+
const responseText = this.extractResponseText(response);
|
|
132
|
+
// Analyze response for chain exploitation
|
|
133
|
+
const analysis = this.analyzer.analyzeChainExploitation(response);
|
|
134
|
+
if (analysis.vulnerable) {
|
|
135
|
+
return {
|
|
136
|
+
vulnerable: true,
|
|
137
|
+
reason: this.determineVulnerabilityReason(analysis),
|
|
138
|
+
vulnerabilityCategories: analysis.vulnerabilityCategories,
|
|
139
|
+
evidence: {
|
|
140
|
+
chainPayload,
|
|
141
|
+
response: responseText.substring(0, 500),
|
|
142
|
+
analysisResult: analysis,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (analysis.safe) {
|
|
147
|
+
return {
|
|
148
|
+
vulnerable: false,
|
|
149
|
+
reason: analysis.chainType === "SAFE_VALIDATION"
|
|
150
|
+
? "validation_only"
|
|
151
|
+
: "chain_rejected",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
vulnerable: false,
|
|
156
|
+
reason: "tool_not_chain_executor",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return {
|
|
161
|
+
vulnerable: false,
|
|
162
|
+
reason: "test_error",
|
|
163
|
+
error: error instanceof Error ? error.message : String(error),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get test payloads for chain exploitation testing
|
|
169
|
+
*/
|
|
170
|
+
getTestPayloads() {
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
name: "single_step_execution",
|
|
174
|
+
payload: '[{"tool": "vulnerable_calculator", "args": {"query": "2+2"}}]',
|
|
175
|
+
description: "Test basic chain execution without allowlist",
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "output_injection",
|
|
179
|
+
payload: '[{"tool": "calc", "args": {"q": "test"}}, {"tool": "exec", "args": {"cmd": "{{output}}"}}]',
|
|
180
|
+
description: "Test output injection via {{output}} template",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "recursive_chain",
|
|
184
|
+
payload: '[{"tool": "vulnerable_chain_executor", "args": {"chain": "[]"}}]',
|
|
185
|
+
description: "Test recursive/circular chain execution",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "arbitrary_tool",
|
|
189
|
+
payload: '[{"tool": "UNKNOWN_MALICIOUS_TOOL_XYZ", "args": {}}]',
|
|
190
|
+
description: "Test acceptance of arbitrary tool names",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "state_poisoning",
|
|
194
|
+
payload: '[{"tool": "vulnerable_config_modifier", "args": {"setting": "admin_mode=true"}}, {"tool": "vulnerable_admin_action", "args": {"action": "read secrets"}}]',
|
|
195
|
+
description: "Test state poisoning between chain steps",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "depth_bypass",
|
|
199
|
+
payload: JSON.stringify(Array(15)
|
|
200
|
+
.fill(null)
|
|
201
|
+
.map(() => ({ tool: "test", args: {} }))),
|
|
202
|
+
description: "Test depth limit bypass with 15 steps",
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Run comprehensive chain exploitation tests on a tool
|
|
208
|
+
*/
|
|
209
|
+
async runChainExploitationTests(callTool, tool) {
|
|
210
|
+
const results = new Map();
|
|
211
|
+
const testPayloads = this.getTestPayloads();
|
|
212
|
+
const chainParam = this.getChainParamName(tool);
|
|
213
|
+
this.log(`Running chain exploitation tests on ${tool.name}`);
|
|
214
|
+
this.log(`Using parameter: ${chainParam}`);
|
|
215
|
+
for (const test of testPayloads) {
|
|
216
|
+
this.log(` Test: ${test.name} - ${test.description}`);
|
|
217
|
+
const result = await this.testChainPayload(callTool, tool, test.payload, chainParam);
|
|
218
|
+
results.set(test.name, result);
|
|
219
|
+
if (this.verbose) {
|
|
220
|
+
// eslint-disable-next-line no-console
|
|
221
|
+
console.log(` Result: ${result.vulnerable ? "VULNERABLE" : "SAFE"} (${result.reason})`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return results;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Summarize chain exploitation test results
|
|
228
|
+
*/
|
|
229
|
+
summarizeResults(results) {
|
|
230
|
+
let vulnerable = 0;
|
|
231
|
+
let safe = 0;
|
|
232
|
+
let errors = 0;
|
|
233
|
+
const vulnerableTests = [];
|
|
234
|
+
const categories = new Set();
|
|
235
|
+
for (const [testName, result] of results) {
|
|
236
|
+
if (result.reason === "test_error") {
|
|
237
|
+
errors++;
|
|
238
|
+
}
|
|
239
|
+
else if (result.vulnerable) {
|
|
240
|
+
vulnerable++;
|
|
241
|
+
vulnerableTests.push(testName);
|
|
242
|
+
result.vulnerabilityCategories?.forEach((c) => categories.add(c));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
safe++;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
total: results.size,
|
|
250
|
+
vulnerable,
|
|
251
|
+
safe,
|
|
252
|
+
errors,
|
|
253
|
+
vulnerableTests,
|
|
254
|
+
vulnerabilityCategories: Array.from(categories),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Tool State Tester
|
|
3
|
+
* Tests for privilege escalation by calling tools in sequence
|
|
4
|
+
*
|
|
5
|
+
* Issue #92, Challenge #7: Cross-tool state-based authorization bypass
|
|
6
|
+
* Detects when one tool can modify shared state that affects another tool's authorization.
|
|
7
|
+
*
|
|
8
|
+
* Attack flow:
|
|
9
|
+
* 1. Call admin_action → should get "access denied"
|
|
10
|
+
* 2. Call config_modifier with "admin_mode=true"
|
|
11
|
+
* 3. Call admin_action again → if now succeeds, VULNERABLE
|
|
12
|
+
*/
|
|
13
|
+
import { CompatibilityCallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { ProgressCallback } from "../../../../lib/assessment/progressTypes.js";
|
|
15
|
+
/**
|
|
16
|
+
* Function type for calling MCP tools
|
|
17
|
+
*/
|
|
18
|
+
export type CallToolFunction = (name: string, params: Record<string, unknown>) => Promise<CompatibilityCallToolResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Result of cross-tool privilege escalation test
|
|
21
|
+
*/
|
|
22
|
+
export interface CrossToolTestResult {
|
|
23
|
+
vulnerable: boolean;
|
|
24
|
+
reason: "privilege_escalation_confirmed" | "escalation_blocked" | "baseline_has_access" | "modifier_rejected" | "test_error";
|
|
25
|
+
evidence?: {
|
|
26
|
+
baseline: string;
|
|
27
|
+
afterModifier: string;
|
|
28
|
+
enableResult?: string;
|
|
29
|
+
};
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Identified tool pair for cross-tool testing
|
|
34
|
+
*/
|
|
35
|
+
export interface ToolPair {
|
|
36
|
+
admin: Tool;
|
|
37
|
+
modifier: Tool;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Configuration for cross-tool state testing
|
|
41
|
+
*/
|
|
42
|
+
export interface CrossToolTestConfig {
|
|
43
|
+
/** Timeout for each tool call in ms (default: 5000) */
|
|
44
|
+
timeout?: number;
|
|
45
|
+
/** Enable verbose logging */
|
|
46
|
+
verbose?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Tests for cross-tool privilege escalation via shared mutable state
|
|
50
|
+
*/
|
|
51
|
+
export declare class CrossToolStateTester {
|
|
52
|
+
private readonly verbose;
|
|
53
|
+
constructor(config?: CrossToolTestConfig);
|
|
54
|
+
/**
|
|
55
|
+
* Log message if verbose logging is enabled
|
|
56
|
+
*/
|
|
57
|
+
private log;
|
|
58
|
+
/**
|
|
59
|
+
* Identify potential cross-tool pairs for testing
|
|
60
|
+
* Looks for admin_action/privileged tools and config_modifier/setting tools
|
|
61
|
+
*/
|
|
62
|
+
identifyCrossToolPairs(tools: Tool[]): ToolPair[];
|
|
63
|
+
/**
|
|
64
|
+
* Test cross-tool privilege escalation
|
|
65
|
+
*
|
|
66
|
+
* Attack flow:
|
|
67
|
+
* 1. Call admin_action → expect "access denied"
|
|
68
|
+
* 2. Call config_modifier with "admin_mode=true"
|
|
69
|
+
* 3. Call admin_action again → if now succeeds, VULNERABLE
|
|
70
|
+
*/
|
|
71
|
+
testPrivilegeEscalation(callTool: CallToolFunction, adminTool: Tool, modifierTool: Tool, onProgress?: ProgressCallback): Promise<CrossToolTestResult>;
|
|
72
|
+
/**
|
|
73
|
+
* Run sequence tests on all identified tool pairs
|
|
74
|
+
*/
|
|
75
|
+
runAllSequenceTests(tools: Tool[], callTool: CallToolFunction, onProgress?: ProgressCallback): Promise<Map<string, CrossToolTestResult>>;
|
|
76
|
+
/**
|
|
77
|
+
* Get summary of sequence test results
|
|
78
|
+
*/
|
|
79
|
+
summarizeResults(results: Map<string, CrossToolTestResult>): {
|
|
80
|
+
total: number;
|
|
81
|
+
vulnerable: number;
|
|
82
|
+
safe: number;
|
|
83
|
+
errors: number;
|
|
84
|
+
vulnerablePairs: string[];
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Extract text content from MCP response
|
|
88
|
+
*/
|
|
89
|
+
private extractResponseText;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=CrossToolStateTester.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CrossToolStateTester.d.ts","sourceRoot":"","sources":["../../../../../src/services/assessment/modules/securityTests/CrossToolStateTester.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EACL,2BAA2B,EAC3B,IAAI,EACL,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC5B,OAAO,CAAC,2BAA2B,CAAC,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EACF,gCAAgC,GAChC,oBAAoB,GACpB,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,CAAC;IACjB,QAAQ,CAAC,EAAE;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,IAAI,CAAC;IACZ,QAAQ,EAAE,IAAI,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;gBAEtB,MAAM,GAAE,mBAAwB;IAK5C;;OAEG;IACH,OAAO,CAAC,GAAG;IAOX;;;OAGG;IACH,sBAAsB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,QAAQ,EAAE;IA8BjD;;;;;;;OAOG;IACG,uBAAuB,CAC3B,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,IAAI,EACf,YAAY,EAAE,IAAI,EAClB,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,mBAAmB,CAAC;IAkI/B;;OAEG;IACG,mBAAmB,CACvB,KAAK,EAAE,IAAI,EAAE,EACb,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,CAAC,EAAE,gBAAgB,GAC5B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAkB5C;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,GAAG;QAC3D,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,eAAe,EAAE,MAAM,EAAE,CAAC;KAC3B;IA0BD;;OAEG;IACH,OAAO,CAAC,mBAAmB;CAkB5B"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-Tool State Tester
|
|
3
|
+
* Tests for privilege escalation by calling tools in sequence
|
|
4
|
+
*
|
|
5
|
+
* Issue #92, Challenge #7: Cross-tool state-based authorization bypass
|
|
6
|
+
* Detects when one tool can modify shared state that affects another tool's authorization.
|
|
7
|
+
*
|
|
8
|
+
* Attack flow:
|
|
9
|
+
* 1. Call admin_action → should get "access denied"
|
|
10
|
+
* 2. Call config_modifier with "admin_mode=true"
|
|
11
|
+
* 3. Call admin_action again → if now succeeds, VULNERABLE
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Tests for cross-tool privilege escalation via shared mutable state
|
|
15
|
+
*/
|
|
16
|
+
export class CrossToolStateTester {
|
|
17
|
+
verbose;
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
// Note: config.timeout is accepted but tool call timeout is handled externally
|
|
20
|
+
this.verbose = config.verbose ?? false;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Log message if verbose logging is enabled
|
|
24
|
+
*/
|
|
25
|
+
log(message) {
|
|
26
|
+
if (this.verbose) {
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.log(`[CrossToolStateTester] ${message}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Identify potential cross-tool pairs for testing
|
|
33
|
+
* Looks for admin_action/privileged tools and config_modifier/setting tools
|
|
34
|
+
*/
|
|
35
|
+
identifyCrossToolPairs(tools) {
|
|
36
|
+
const pairs = [];
|
|
37
|
+
// Find admin-like tools (tools that check authorization)
|
|
38
|
+
const adminTools = tools.filter((t) => /admin[_-]?action|privileged|elevated|sensitive/i.test(t.name) ||
|
|
39
|
+
/admin[_-]?action|privileged|requires\s+admin/i.test(t.description || ""));
|
|
40
|
+
// Find modifier-like tools (tools that modify state/config)
|
|
41
|
+
const modifierTools = tools.filter((t) => /config[_-]?modifier|setting|mode|enable|activate/i.test(t.name) ||
|
|
42
|
+
/modify.*config|set.*mode|enable.*admin/i.test(t.description || ""));
|
|
43
|
+
for (const admin of adminTools) {
|
|
44
|
+
for (const modifier of modifierTools) {
|
|
45
|
+
if (admin.name !== modifier.name) {
|
|
46
|
+
pairs.push({ admin, modifier });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return pairs;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Test cross-tool privilege escalation
|
|
54
|
+
*
|
|
55
|
+
* Attack flow:
|
|
56
|
+
* 1. Call admin_action → expect "access denied"
|
|
57
|
+
* 2. Call config_modifier with "admin_mode=true"
|
|
58
|
+
* 3. Call admin_action again → if now succeeds, VULNERABLE
|
|
59
|
+
*/
|
|
60
|
+
async testPrivilegeEscalation(callTool, adminTool, modifierTool, onProgress) {
|
|
61
|
+
this.log(`Testing pair: ${modifierTool.name} → ${adminTool.name}`);
|
|
62
|
+
try {
|
|
63
|
+
// Step 1: Baseline - admin action should be denied
|
|
64
|
+
this.log(`Step 1: Calling ${adminTool.name} (baseline)`);
|
|
65
|
+
const baseline = await callTool(adminTool.name, {
|
|
66
|
+
action: "read secrets",
|
|
67
|
+
});
|
|
68
|
+
const baselineText = this.extractResponseText(baseline);
|
|
69
|
+
const baselineDenied = /access denied|unauthorized|admin.*required|permission.*denied/i.test(baselineText);
|
|
70
|
+
if (!baselineDenied) {
|
|
71
|
+
// Already has access - can't test escalation (might indicate other issues)
|
|
72
|
+
// Check if it's because shared_state_checked is false (hardened)
|
|
73
|
+
if (/"shared_state_checked"\s*:\s*false/i.test(baselineText)) {
|
|
74
|
+
return {
|
|
75
|
+
vulnerable: false,
|
|
76
|
+
reason: "baseline_has_access",
|
|
77
|
+
evidence: {
|
|
78
|
+
baseline: baselineText.slice(0, 500),
|
|
79
|
+
afterModifier: "",
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
vulnerable: false,
|
|
85
|
+
reason: "baseline_has_access",
|
|
86
|
+
evidence: {
|
|
87
|
+
baseline: baselineText.slice(0, 500),
|
|
88
|
+
afterModifier: "",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Step 2: Attempt to enable admin mode via config modifier
|
|
93
|
+
const enableResult = await callTool(modifierTool.name, {
|
|
94
|
+
setting: "admin_mode=true",
|
|
95
|
+
});
|
|
96
|
+
const enableText = this.extractResponseText(enableResult);
|
|
97
|
+
const modeEnabled = /admin.*activated|elevated.*granted|admin_mode.*enabled|configuration.*updated.*admin/i.test(enableText);
|
|
98
|
+
// Check if modifier is hardened (stores without modifying)
|
|
99
|
+
const modifierHardened = /stored.*without.*modifying|stored.*for.*review|not.*modifying.*runtime/i.test(enableText);
|
|
100
|
+
if (modifierHardened) {
|
|
101
|
+
return {
|
|
102
|
+
vulnerable: false,
|
|
103
|
+
reason: "modifier_rejected",
|
|
104
|
+
evidence: {
|
|
105
|
+
baseline: baselineText.slice(0, 300),
|
|
106
|
+
afterModifier: "",
|
|
107
|
+
enableResult: enableText.slice(0, 300),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!modeEnabled) {
|
|
112
|
+
return {
|
|
113
|
+
vulnerable: false,
|
|
114
|
+
reason: "modifier_rejected",
|
|
115
|
+
evidence: {
|
|
116
|
+
baseline: baselineText.slice(0, 300),
|
|
117
|
+
afterModifier: "",
|
|
118
|
+
enableResult: enableText.slice(0, 300),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Step 3: Test if admin action now succeeds
|
|
123
|
+
const escalated = await callTool(adminTool.name, {
|
|
124
|
+
action: "read secrets",
|
|
125
|
+
});
|
|
126
|
+
const escalatedText = this.extractResponseText(escalated);
|
|
127
|
+
// Check for successful escalation
|
|
128
|
+
const escalationSucceeded = !/access denied|unauthorized|admin.*required|permission.*denied/i.test(escalatedText) &&
|
|
129
|
+
/"secrets"|API_KEY|credentials|admin_mode.*true.*evidence.*cross-tool/i.test(escalatedText);
|
|
130
|
+
// Emit progress event if callback provided
|
|
131
|
+
if (onProgress && escalationSucceeded) {
|
|
132
|
+
onProgress({
|
|
133
|
+
type: "vulnerability_found",
|
|
134
|
+
tool: adminTool.name,
|
|
135
|
+
pattern: "Cross-Tool State Bypass",
|
|
136
|
+
confidence: "high",
|
|
137
|
+
evidence: `Cross-tool privilege escalation: ${modifierTool.name} enables access to ${adminTool.name}. ${escalatedText.slice(0, 200)}`,
|
|
138
|
+
riskLevel: "HIGH",
|
|
139
|
+
requiresReview: false,
|
|
140
|
+
payload: "admin_mode=true",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
vulnerable: escalationSucceeded,
|
|
145
|
+
reason: escalationSucceeded
|
|
146
|
+
? "privilege_escalation_confirmed"
|
|
147
|
+
: "escalation_blocked",
|
|
148
|
+
evidence: {
|
|
149
|
+
baseline: baselineText.slice(0, 300),
|
|
150
|
+
afterModifier: escalatedText.slice(0, 300),
|
|
151
|
+
enableResult: enableText.slice(0, 300),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return {
|
|
157
|
+
vulnerable: false,
|
|
158
|
+
reason: "test_error",
|
|
159
|
+
error: error instanceof Error ? error.message : String(error),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Run sequence tests on all identified tool pairs
|
|
165
|
+
*/
|
|
166
|
+
async runAllSequenceTests(tools, callTool, onProgress) {
|
|
167
|
+
const pairs = this.identifyCrossToolPairs(tools);
|
|
168
|
+
const results = new Map();
|
|
169
|
+
for (const { admin, modifier } of pairs) {
|
|
170
|
+
const key = `${modifier.name} → ${admin.name}`;
|
|
171
|
+
const result = await this.testPrivilegeEscalation(callTool, admin, modifier, onProgress);
|
|
172
|
+
results.set(key, result);
|
|
173
|
+
}
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Get summary of sequence test results
|
|
178
|
+
*/
|
|
179
|
+
summarizeResults(results) {
|
|
180
|
+
let vulnerable = 0;
|
|
181
|
+
let safe = 0;
|
|
182
|
+
let errors = 0;
|
|
183
|
+
const vulnerablePairs = [];
|
|
184
|
+
for (const [key, result] of results) {
|
|
185
|
+
if (result.reason === "test_error") {
|
|
186
|
+
errors++;
|
|
187
|
+
}
|
|
188
|
+
else if (result.vulnerable) {
|
|
189
|
+
vulnerable++;
|
|
190
|
+
vulnerablePairs.push(key);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
safe++;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
total: results.size,
|
|
198
|
+
vulnerable,
|
|
199
|
+
safe,
|
|
200
|
+
errors,
|
|
201
|
+
vulnerablePairs,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract text content from MCP response
|
|
206
|
+
*/
|
|
207
|
+
extractResponseText(response) {
|
|
208
|
+
if (!response)
|
|
209
|
+
return "";
|
|
210
|
+
// Handle content array format
|
|
211
|
+
if (response.content && Array.isArray(response.content)) {
|
|
212
|
+
return response.content
|
|
213
|
+
.map((item) => {
|
|
214
|
+
if (typeof item === "string")
|
|
215
|
+
return item;
|
|
216
|
+
if (item && typeof item === "object" && "text" in item)
|
|
217
|
+
return String(item.text);
|
|
218
|
+
return JSON.stringify(item);
|
|
219
|
+
})
|
|
220
|
+
.join("\n");
|
|
221
|
+
}
|
|
222
|
+
// Fallback to JSON stringify
|
|
223
|
+
return JSON.stringify(response);
|
|
224
|
+
}
|
|
225
|
+
}
|