@bryan-thompson/inspector-assessment-client 1.26.4 → 1.26.6

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.
Files changed (30) hide show
  1. package/dist/assets/{OAuthCallback-DRmaIku9.js → OAuthCallback-CCWVtjr7.js} +1 -1
  2. package/dist/assets/{OAuthDebugCallback-BU8UZdx8.js → OAuthDebugCallback-DqbXfUi4.js} +1 -1
  3. package/dist/assets/{index-Dd4pL57l.js → index-CsDJSSWq.js} +4 -4
  4. package/dist/index.html +1 -1
  5. package/lib/lib/securityPatterns.d.ts.map +1 -1
  6. package/lib/lib/securityPatterns.js +26 -0
  7. package/lib/services/assessment/modules/securityTests/ConfidenceScorer.d.ts +57 -0
  8. package/lib/services/assessment/modules/securityTests/ConfidenceScorer.d.ts.map +1 -0
  9. package/lib/services/assessment/modules/securityTests/ConfidenceScorer.js +199 -0
  10. package/lib/services/assessment/modules/securityTests/ErrorClassifier.d.ts +57 -0
  11. package/lib/services/assessment/modules/securityTests/ErrorClassifier.d.ts.map +1 -0
  12. package/lib/services/assessment/modules/securityTests/ErrorClassifier.js +113 -0
  13. package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.d.ts +49 -0
  14. package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.d.ts.map +1 -0
  15. package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.js +74 -0
  16. package/lib/services/assessment/modules/securityTests/MathAnalyzer.d.ts +58 -0
  17. package/lib/services/assessment/modules/securityTests/MathAnalyzer.d.ts.map +1 -0
  18. package/lib/services/assessment/modules/securityTests/MathAnalyzer.js +251 -0
  19. package/lib/services/assessment/modules/securityTests/SafeResponseDetector.d.ts +59 -0
  20. package/lib/services/assessment/modules/securityTests/SafeResponseDetector.d.ts.map +1 -0
  21. package/lib/services/assessment/modules/securityTests/SafeResponseDetector.js +151 -0
  22. package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts +229 -0
  23. package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts.map +1 -0
  24. package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.js +566 -0
  25. package/lib/services/assessment/modules/securityTests/SecurityPayloadGenerator.d.ts.map +1 -1
  26. package/lib/services/assessment/modules/securityTests/SecurityPayloadGenerator.js +49 -1
  27. package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts +63 -85
  28. package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts.map +1 -1
  29. package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.js +270 -1159
  30. package/package.json +1 -1
@@ -1,16 +1,48 @@
1
1
  /**
2
- * Security Response Analyzer
2
+ * Security Response Analyzer (Facade)
3
3
  * Analyzes tool responses for evidence-based vulnerability detection
4
4
  *
5
- * Extracted from SecurityAssessor.ts for maintainability.
6
- * Handles response analysis, reflection detection, and confidence calculation.
5
+ * REFACTORED in Issue #53 (v2.0.0): Converted to facade pattern
6
+ * Delegates to focused classes for maintainability (CC 218 → ~50)
7
+ *
8
+ * Extracted classes:
9
+ * - ErrorClassifier: Error classification and connection error detection
10
+ * - ExecutionArtifactDetector: Execution evidence detection
11
+ * - MathAnalyzer: Math computation detection (Calculator Injection)
12
+ * - SafeResponseDetector: Safe response pattern detection
13
+ * - ConfidenceScorer: Confidence level calculation
7
14
  */
8
15
  import { ToolClassifier, ToolCategory } from "../../ToolClassifier.js";
16
+ // Import extracted classes
17
+ import { ErrorClassifier } from "./ErrorClassifier.js";
18
+ import { ExecutionArtifactDetector } from "./ExecutionArtifactDetector.js";
19
+ import { MathAnalyzer } from "./MathAnalyzer.js";
20
+ import { SafeResponseDetector } from "./SafeResponseDetector.js";
21
+ import { ConfidenceScorer } from "./ConfidenceScorer.js";
9
22
  /**
10
23
  * Analyzes tool responses for security vulnerabilities
11
24
  * Distinguishes between safe reflection and actual execution
25
+ *
26
+ * This class serves as a facade, delegating to focused analyzers
27
+ * while maintaining the same public API for backward compatibility.
12
28
  */
13
29
  export class SecurityResponseAnalyzer {
30
+ // Delegate classes
31
+ errorClassifier;
32
+ executionDetector;
33
+ mathAnalyzer;
34
+ safeDetector;
35
+ confidenceScorer;
36
+ constructor() {
37
+ this.errorClassifier = new ErrorClassifier();
38
+ this.executionDetector = new ExecutionArtifactDetector();
39
+ this.mathAnalyzer = new MathAnalyzer();
40
+ this.safeDetector = new SafeResponseDetector();
41
+ this.confidenceScorer = new ConfidenceScorer();
42
+ }
43
+ // ============================================================================
44
+ // PUBLIC API - These 8 methods maintain backward compatibility
45
+ // ============================================================================
14
46
  /**
15
47
  * Analyze response with evidence-based detection
16
48
  * CRITICAL: Distinguish between safe reflection and actual execution
@@ -20,7 +52,7 @@ export class SecurityResponseAnalyzer {
20
52
  */
21
53
  analyzeResponse(response, payload, tool) {
22
54
  const responseText = this.extractResponseContent(response).toLowerCase();
23
- const errorInfo = this.extractErrorInfo(response);
55
+ const errorInfo = this.errorClassifier.extractErrorInfo(response);
24
56
  // Check 1: Safe error responses (MCP validation, HTTP errors)
25
57
  const errorResult = this.checkSafeErrorResponses(responseText, errorInfo);
26
58
  if (errorResult)
@@ -33,1265 +65,344 @@ export class SecurityResponseAnalyzer {
33
65
  return this.checkVulnerabilityEvidence(response, payload, responseText);
34
66
  }
35
67
  /**
36
- * Check for safe error responses that indicate proper input rejection
37
- * Handles: MCP validation errors (-32602), HTTP 4xx/5xx errors
68
+ * Calculate confidence level and manual review requirements
38
69
  */
39
- checkSafeErrorResponses(responseText, errorInfo) {
40
- // STEP 1: Check for MCP validation errors (HIGHEST PRIORITY)
41
- // Tools that reject invalid input before processing are SECURE
42
- if (this.isMCPValidationError(errorInfo, responseText)) {
43
- const errorCode = errorInfo.code ? ` (${errorInfo.code})` : "";
44
- return {
45
- isVulnerable: false,
46
- evidence: `MCP validation error${errorCode}: Tool properly rejected invalid input before processing`,
47
- };
48
- }
49
- // ✅ STEP 1.1: Check for HTTP error responses (Issue #26)
50
- // HTTP 4xx/5xx errors indicate tool rejection, not vulnerability
51
- if (this.isHttpErrorResponse(responseText)) {
52
- return {
53
- isVulnerable: false,
54
- evidence: "HTTP error response indicates tool rejection (not vulnerability)",
55
- };
56
- }
57
- return null; // Continue to next check
70
+ calculateConfidence(tool, isVulnerable, evidence, responseText, payload, sanitizationResult) {
71
+ return this.confidenceScorer.calculateConfidence(tool, isVulnerable, evidence, responseText, payload, sanitizationResult);
58
72
  }
59
73
  /**
60
- * Check for safe tool behavior patterns
61
- * Handles: Tool categories, reflection, computed math, validation rejection
74
+ * Analyze response for auth bypass patterns (Issue #75)
75
+ * Detects fail-open authentication vulnerabilities (CVE-2025-52882)
62
76
  */
63
- checkSafeToolBehavior(response, payload, tool, responseText) {
64
- // STEP 1.5: Classify tool and check for safe categories (prevents false positives)
65
- const classifier = new ToolClassifier();
66
- const classification = classifier.classify(tool.name, tool.description);
67
- // Check if tool is in a safe category and response matches expected format
68
- if (classification.categories.includes(ToolCategory.SEARCH_RETRIEVAL)) {
69
- if (this.isSearchResultResponse(responseText)) {
70
- return {
71
- isVulnerable: false,
72
- evidence: "Search tool returned query results (not code execution)",
73
- };
77
+ analyzeAuthBypassResponse(response) {
78
+ const responseText = this.extractResponseContent(response);
79
+ // Fail-open indicators (VULNERABLE - CVE-2025-52882 pattern)
80
+ const failOpenPatterns = [
81
+ {
82
+ pattern: /"auth_type"\s*:\s*"fail-open"/i,
83
+ evidence: "auth_type: fail-open (CVE-2025-52882)",
84
+ },
85
+ {
86
+ pattern: /"auth_status"\s*:\s*"bypassed"/i,
87
+ evidence: "auth_status: bypassed",
88
+ },
89
+ {
90
+ pattern: /access.*granted.*despite/i,
91
+ evidence: "access granted despite failure",
92
+ },
93
+ {
94
+ pattern: /authentication.*skipped/i,
95
+ evidence: "authentication skipped",
96
+ },
97
+ { pattern: /fail[\s-]?open/i, evidence: "fail-open pattern detected" },
98
+ { pattern: /bypassed.*auth/i, evidence: "auth bypassed" },
99
+ {
100
+ pattern: /authentication.*bypassed/i,
101
+ evidence: "authentication bypassed",
102
+ },
103
+ {
104
+ pattern: /"vulnerable"\s*:\s*true[^}]*(?:"auth_status"|"auth_type"|"auth_error")|(?:"auth_status"|"auth_type"|"auth_error")[^}]*"vulnerable"\s*:\s*true/i,
105
+ evidence: "vulnerable flag with auth context",
106
+ },
107
+ {
108
+ pattern: /auth.*succeeded.*null/i,
109
+ evidence: "auth succeeded with null token",
110
+ },
111
+ {
112
+ pattern: /granted.*without.*valid/i,
113
+ evidence: "granted without valid token",
114
+ },
115
+ {
116
+ pattern: /"action_performed"/i,
117
+ evidence: "action performed indicator",
118
+ },
119
+ ];
120
+ // Fail-closed indicators (SAFE - proper auth handling)
121
+ const failClosedPatterns = [
122
+ {
123
+ pattern: /"auth_type"\s*:\s*"fail-closed"/i,
124
+ evidence: "auth_type: fail-closed (secure)",
125
+ },
126
+ {
127
+ pattern: /"auth_status"\s*:\s*"denied"/i,
128
+ evidence: "auth_status: denied",
129
+ },
130
+ { pattern: /access.*denied/i, evidence: "access denied" },
131
+ { pattern: /authentication.*failed/i, evidence: "authentication failed" },
132
+ {
133
+ pattern: /fail[\s-]?closed/i,
134
+ evidence: "fail-closed pattern detected",
135
+ },
136
+ { pattern: /"status"\s*:\s*"blocked"/i, evidence: "status: blocked" },
137
+ { pattern: /invalid.*token/i, evidence: "invalid token rejection" },
138
+ { pattern: /token.*required/i, evidence: "token required" },
139
+ { pattern: /unauthorized/i, evidence: "unauthorized response" },
140
+ { pattern: /"denial_reason"/i, evidence: "denial reason provided" },
141
+ ];
142
+ // Check for fail-open (vulnerable) patterns first
143
+ for (const { pattern, evidence } of failOpenPatterns) {
144
+ if (pattern.test(responseText)) {
145
+ return { detected: true, failureMode: "FAIL_OPEN", evidence };
74
146
  }
75
147
  }
76
- if (classification.categories.includes(ToolCategory.CRUD_CREATION)) {
77
- if (this.isCreationResponse(responseText)) {
78
- return {
79
- isVulnerable: false,
80
- evidence: "CRUD tool created/modified resource (not code execution)",
81
- };
148
+ // Check for fail-closed (safe) patterns
149
+ for (const { pattern, evidence } of failClosedPatterns) {
150
+ if (pattern.test(responseText)) {
151
+ return { detected: false, failureMode: "FAIL_CLOSED", evidence };
82
152
  }
83
153
  }
84
- if (classification.categories.includes(ToolCategory.READ_ONLY_INFO)) {
85
- return {
86
- isVulnerable: false,
87
- evidence: "Read-only info tool returned user/workspace data (intended data exposure, not vulnerability)",
88
- };
89
- }
90
- if (classification.categories.includes(ToolCategory.SAFE_STORAGE)) {
91
- return {
92
- isVulnerable: false,
93
- evidence: "Safe storage control tool (validated safe implementation, control group for testing)",
94
- };
95
- }
96
- // ✅ STEP 2: Check if response is just reflecting/echoing input (SAFE)
97
- const isJustReflection = this.isReflectionResponse(responseText);
98
- if (isJustReflection) {
99
- return {
100
- isVulnerable: false,
101
- evidence: "Tool safely reflected input without execution",
102
- };
103
- }
104
- // ✅ STEP 1.7: Check for computed math results (Issue #14 fix, enhanced in Issue #58)
105
- // Use enhanced analysis with tool context and confidence levels
106
- const mathAnalysis = this.analyzeComputedMathResult(payload.payload, responseText, tool);
107
- // Only flag as vulnerable if HIGH confidence (Issue #58 fix)
108
- // Low/medium confidence excluded to prevent false positives on API wrapper tools
109
- if (mathAnalysis.isComputed && mathAnalysis.confidence === "high") {
110
- return {
111
- isVulnerable: true,
112
- evidence: `Tool computed math expression result instead of storing/echoing it (payload: ${payload.payload})`,
113
- };
114
- }
115
- // ✅ STEP 2: Check if tool explicitly rejected the input (SAFE)
116
- if (this.isValidationRejection(response)) {
117
- return {
118
- isVulnerable: false,
119
- evidence: "Tool correctly rejected invalid input with validation error",
120
- };
121
- }
122
- return null; // Continue to next check
154
+ return { detected: false, failureMode: "UNKNOWN" };
123
155
  }
124
156
  /**
125
- * Check for vulnerability evidence in response
126
- * Handles: Evidence pattern matching, fallback injection analysis
157
+ * Check if response indicates connection/server failure
127
158
  */
128
- checkVulnerabilityEvidence(response, payload, responseText) {
129
- const payloadText = payload.payload.toLowerCase();
130
- // ✅ STEP 3: Check for expected evidence of execution
131
- if (payload.evidence && payload.evidence.test(responseText)) {
132
- const evidenceMatchesInput = payload.evidence.test(payloadText);
133
- if (evidenceMatchesInput && responseText.includes(payloadText)) {
134
- return {
135
- isVulnerable: false,
136
- evidence: "Evidence pattern matched echoed input, not execution",
137
- };
138
- }
139
- // For validation-type patterns, require execution evidence
140
- if (this.isValidationPattern(payload.evidence)) {
141
- const hasExecutionEvidence = this.hasExecutionEvidence(responseText);
142
- if (!hasExecutionEvidence) {
143
- return {
144
- isVulnerable: false,
145
- evidence: "Pattern matched in error message but no execution evidence - tool properly validated input",
146
- };
147
- }
148
- }
149
- return {
150
- isVulnerable: true,
151
- evidence: `Tool executed malicious instruction: found evidence matching ${payload.evidence} - ${payload.description}`,
152
- };
153
- }
154
- // ✅ STEP 4: Fall back to existing detection logic
155
- return this.analyzeInjectionResponse(response, payload.payload);
159
+ isConnectionError(response) {
160
+ return this.errorClassifier.isConnectionError(response);
156
161
  }
157
162
  /**
158
- * Check if tool explicitly rejected input with validation error (SAFE)
163
+ * Check if caught exception indicates connection/server failure
159
164
  */
160
- isValidationRejection(response) {
161
- const responseText = this.extractResponseContent(response);
162
- try {
163
- const parsed = JSON.parse(responseText);
164
- if (parsed.valid === false ||
165
- parsed.error === true ||
166
- parsed.error === "true" ||
167
- (parsed.error && parsed.error !== false) ||
168
- parsed.status === "rejected" ||
169
- parsed.status === "invalid" ||
170
- parsed.status === "failed") {
171
- return true;
172
- }
173
- if (parsed.errors &&
174
- Array.isArray(parsed.errors) &&
175
- parsed.errors.length > 0) {
176
- return true;
177
- }
178
- if (parsed.error && typeof parsed.error === "string") {
179
- return true;
180
- }
181
- if (typeof parsed.result === "string") {
182
- const resultRejectionPatterns = [
183
- /validation (failed|error)/i,
184
- /rejected/i,
185
- /not.*approved/i,
186
- /not.*in.*list/i,
187
- /invalid.*input/i,
188
- /error:.*invalid/i,
189
- ];
190
- if (resultRejectionPatterns.some((p) => p.test(parsed.result))) {
191
- return true;
192
- }
193
- }
194
- }
195
- catch {
196
- // Not JSON, check text patterns
197
- }
198
- const rejectionPatterns = [
199
- /validation failed/i,
200
- /rejected/i,
201
- /not.*approved/i,
202
- /not.*in.*list/i,
203
- /invalid.*input/i,
204
- /error:.*invalid/i,
205
- ];
206
- return rejectionPatterns.some((pattern) => pattern.test(responseText));
165
+ isConnectionErrorFromException(error) {
166
+ return this.errorClassifier.isConnectionErrorFromException(error);
167
+ }
168
+ /**
169
+ * Classify error type for reporting
170
+ */
171
+ classifyError(response) {
172
+ return this.errorClassifier.classifyError(response);
173
+ }
174
+ /**
175
+ * Classify error type from caught exception
176
+ */
177
+ classifyErrorFromException(error) {
178
+ return this.errorClassifier.classifyErrorFromException(error);
179
+ }
180
+ /**
181
+ * Extract response content from MCP response
182
+ */
183
+ extractResponseContent(response) {
184
+ return this.safeDetector.extractResponseContent(response);
207
185
  }
186
+ // ============================================================================
187
+ // DELEGATED PUBLIC METHODS - Exposed for external use
188
+ // ============================================================================
208
189
  /**
209
190
  * Check if response is an MCP validation error (safe rejection)
210
191
  */
211
192
  isMCPValidationError(errorInfo, responseText) {
212
- if (errorInfo.code === -32602 || errorInfo.code === "-32602") {
213
- return true;
214
- }
215
- const validationPatterns = [
216
- /parameter validation failed/i,
217
- /schema validation (error|failed)/i,
218
- /invalid (url|email|format|parameter|input|data)/i,
219
- /must be a valid/i,
220
- /must have a valid/i,
221
- /failed to validate/i,
222
- /validation error/i,
223
- /does not match (pattern|schema)/i,
224
- /not a valid (url|email|number|string)/i,
225
- /expected.*but (got|received)/i,
226
- /type mismatch/i,
227
- /\brequired\b.*\bmissing\b/i,
228
- /cannot.*be.*empty/i,
229
- /must.*not.*be.*empty/i,
230
- /empty.*not.*allowed/i,
231
- /\brequired\b/i,
232
- /missing.*required/i,
233
- /field.*required/i,
234
- ];
235
- return validationPatterns.some((pattern) => pattern.test(responseText));
193
+ return this.safeDetector.isMCPValidationError(errorInfo, responseText);
236
194
  }
237
195
  /**
238
196
  * Check if response is an HTTP error (Issue #26)
239
197
  */
240
198
  isHttpErrorResponse(responseText) {
241
- const httpErrorPattern = /\b(4\d{2}|5\d{2})\b.*?(not found|error|bad request|unauthorized|forbidden|internal server|unavailable|timeout|service)/i;
242
- const simpleHttpPattern = /^(4\d{2}|5\d{2})[\s:]/;
243
- const notFoundPattern = /not found/i;
244
- const isShortNotFound = notFoundPattern.test(responseText) && responseText.length < 100;
245
- const jsonStatusPattern = /"status":\s*(4\d{2}|5\d{2})/;
246
- return (httpErrorPattern.test(responseText) ||
247
- simpleHttpPattern.test(responseText) ||
248
- isShortNotFound ||
249
- jsonStatusPattern.test(responseText));
199
+ return this.safeDetector.isHttpErrorResponse(responseText);
250
200
  }
251
201
  /**
252
202
  * Check if evidence pattern is ambiguous
253
203
  */
254
204
  isValidationPattern(evidencePattern) {
255
- const patternStr = evidencePattern.toString().toLowerCase();
256
- const ambiguousPatterns = [
257
- "type.*error",
258
- "invalid.*type",
259
- "error",
260
- "invalid",
261
- "failed",
262
- "negative.*not.*allowed",
263
- "must.*be.*positive",
264
- "invalid.*value",
265
- "overflow",
266
- "out.*of.*range",
267
- ];
268
- return ambiguousPatterns.some((ambiguous) => patternStr.includes(ambiguous));
205
+ return this.confidenceScorer.isValidationPattern(evidencePattern);
269
206
  }
270
207
  /**
271
208
  * Check if response contains evidence of actual execution
272
209
  */
273
210
  hasExecutionEvidence(responseText) {
274
- const executionIndicators = [
275
- /\bexecuted\b/i,
276
- /\bprocessed\b/i,
277
- /\bran\b.*command/i,
278
- /\bcompleted\b/i,
279
- /\bcomputed\b/i,
280
- /\bcalculated\b/i,
281
- /NullPointerException/i,
282
- /SegmentationFault/i,
283
- /StackOverflow/i,
284
- /OutOfMemory/i,
285
- /syntax error in executed/i,
286
- /error while executing/i,
287
- /failed during execution/i,
288
- /error in query execution/i,
289
- /runtime error/i,
290
- /deleted \d+ (rows|files|records)/i,
291
- /(file|resource) (opened|accessed|modified|deleted)/i,
292
- /query returned \d+ results/i,
293
- /modified \d+ records/i,
294
- /\d+ rows affected/i,
295
- /command output:/i,
296
- /execution result:/i,
297
- ];
298
- return executionIndicators.some((pattern) => pattern.test(responseText));
211
+ return this.executionDetector.hasExecutionEvidence(responseText);
299
212
  }
300
213
  /**
301
214
  * Check if a math expression payload was computed (execution evidence)
215
+ * @deprecated Use analyzeComputedMathResult instead
302
216
  */
303
217
  isComputedMathResult(payload, responseText) {
304
- // Skip HTTP error responses
305
- const httpErrorPattern = /\b(4\d{2}|5\d{2})\b.*?(not found|error|bad request|unauthorized|forbidden|internal server|unavailable|timeout)/i;
306
- const simpleHttpPattern = /^(4\d{2}|5\d{2})[\s:]/;
307
- const notFoundPattern = /not found/i;
308
- if (httpErrorPattern.test(responseText) ||
309
- simpleHttpPattern.test(responseText) ||
310
- (notFoundPattern.test(responseText) && responseText.length < 100)) {
311
- return false;
312
- }
313
- const simpleMathPattern = /^\s*(\d+)\s*([+\-*/])\s*(\d+)(?:\s*([+\-*/])\s*(\d+))?\s*$/;
314
- const match = payload.match(simpleMathPattern);
315
- if (!match) {
316
- return false;
317
- }
318
- try {
319
- const num1 = parseInt(match[1], 10);
320
- const op1 = match[2];
321
- const num2 = parseInt(match[3], 10);
322
- const op2 = match[4];
323
- const num3 = match[5] ? parseInt(match[5], 10) : undefined;
324
- let result;
325
- switch (op1) {
326
- case "+":
327
- result = num1 + num2;
328
- break;
329
- case "-":
330
- result = num1 - num2;
331
- break;
332
- case "*":
333
- result = num1 * num2;
334
- break;
335
- case "/":
336
- result = Math.floor(num1 / num2);
337
- break;
338
- default:
339
- return false;
340
- }
341
- if (op2 && num3 !== undefined) {
342
- switch (op2) {
343
- case "+":
344
- result = result + num3;
345
- break;
346
- case "-":
347
- result = result - num3;
348
- break;
349
- case "*":
350
- result = result * num3;
351
- break;
352
- case "/":
353
- result = Math.floor(result / num3);
354
- break;
355
- default:
356
- return false;
357
- }
358
- }
359
- const resultStr = result.toString();
360
- const hasComputedResult = responseText.includes(resultStr);
361
- const normalizedPayload = payload.replace(/\s+/g, "");
362
- const hasOriginalExpression = responseText.includes(payload) ||
363
- responseText.includes(normalizedPayload);
364
- return hasComputedResult && !hasOriginalExpression;
365
- }
366
- catch {
367
- return false;
368
- }
218
+ return this.mathAnalyzer.isComputedMathResult(payload, responseText);
369
219
  }
370
220
  /**
371
- * Check if numeric value appears in structured data context (not as computation result)
372
- * Distinguishes {"records": 4} from computed "4" (Issue #58)
373
- *
374
- * @param result The computed numeric result to check for
375
- * @param responseText The response text to analyze
376
- * @returns true if the number appears to be coincidental data, not a computed result
221
+ * Check if numeric value appears in structured data context
377
222
  */
378
223
  isCoincidentalNumericInStructuredData(result, responseText) {
379
- // Common data field names that often contain numeric values
380
- const dataFieldPatterns = [
381
- "count",
382
- "total",
383
- "records",
384
- "page",
385
- "limit",
386
- "offset",
387
- "id",
388
- "status",
389
- "code",
390
- "version",
391
- "index",
392
- "size",
393
- "employees",
394
- "items",
395
- "results",
396
- "entries",
397
- "length",
398
- "pages",
399
- "rows",
400
- "columns",
401
- "width",
402
- "height",
403
- "timestamp",
404
- "duration",
405
- "amount",
406
- "price",
407
- "quantity",
408
- ];
409
- // Try to parse as JSON
410
- try {
411
- const parsed = JSON.parse(responseText);
412
- const checkObject = (obj, depth = 0) => {
413
- if (depth > 5)
414
- return false; // Prevent deep recursion
415
- if (typeof obj !== "object" || obj === null)
416
- return false;
417
- for (const [key, value] of Object.entries(obj)) {
418
- // Check if numeric value matches result and key is a data field
419
- if (value === result) {
420
- const keyLower = key.toLowerCase();
421
- if (dataFieldPatterns.some((pattern) => keyLower.includes(pattern))) {
422
- return true;
423
- }
424
- }
425
- // Recurse into nested objects
426
- if (typeof value === "object" && value !== null) {
427
- if (checkObject(value, depth + 1))
428
- return true;
429
- }
430
- // Check arrays
431
- if (Array.isArray(value)) {
432
- for (const item of value) {
433
- if (typeof item === "object" && checkObject(item, depth + 1)) {
434
- return true;
435
- }
436
- }
437
- }
438
- }
439
- return false;
440
- };
441
- return checkObject(parsed);
442
- }
443
- catch {
444
- // Not JSON - check for structured text patterns
445
- // e.g., "Records: 4" or "Page 1 of 4" or "Total: 4 items"
446
- const structuredPatterns = [
447
- new RegExp(`(records|count|total|page|items|results|employees|entries|rows)[:\\s]+${result}\\b`, "i"),
448
- new RegExp(`\\b${result}\\s+(records|items|results|entries|employees|rows)\\b`, "i"),
449
- new RegExp(`page\\s+\\d+\\s+of\\s+${result}\\b`, "i"),
450
- new RegExp(`total[:\\s]+${result}\\b`, "i"),
451
- new RegExp(`found\\s+${result}\\s+(results|items|entries)`, "i"),
452
- ];
453
- return structuredPatterns.some((pattern) => pattern.test(responseText));
454
- }
224
+ return this.mathAnalyzer.isCoincidentalNumericInStructuredData(result, responseText);
455
225
  }
456
226
  /**
457
227
  * Enhanced computed math result analysis with tool context (Issue #58)
458
- *
459
- * Returns a confidence level indicating how likely this is a real Calculator Injection:
460
- * - high: Strong evidence of computation (should flag as vulnerable)
461
- * - medium: Ambiguous (excluded from vulnerability count per user decision)
462
- * - low: Likely coincidental data (excluded from vulnerability count)
463
228
  */
464
229
  analyzeComputedMathResult(payload, responseText, tool) {
465
- // Skip HTTP error responses
466
- const httpErrorPattern = /\b(4\d{2}|5\d{2})\b.*?(not found|error|bad request|unauthorized|forbidden|internal server|unavailable|timeout)/i;
467
- const simpleHttpPattern = /^(4\d{2}|5\d{2})[\s:]/;
468
- const notFoundPattern = /not found/i;
469
- if (httpErrorPattern.test(responseText) ||
470
- simpleHttpPattern.test(responseText) ||
471
- (notFoundPattern.test(responseText) && responseText.length < 100)) {
472
- return {
473
- isComputed: false,
474
- confidence: "high",
475
- reason: "HTTP error response",
476
- };
477
- }
478
- const simpleMathPattern = /^\s*(\d+)\s*([+\-*/])\s*(\d+)(?:\s*([+\-*/])\s*(\d+))?\s*$/;
479
- const match = payload.match(simpleMathPattern);
480
- if (!match) {
481
- return {
482
- isComputed: false,
483
- confidence: "high",
484
- reason: "Not a math expression",
485
- };
486
- }
487
- try {
488
- const num1 = parseInt(match[1], 10);
489
- const op1 = match[2];
490
- const num2 = parseInt(match[3], 10);
491
- const op2 = match[4];
492
- const num3 = match[5] ? parseInt(match[5], 10) : undefined;
493
- let result;
494
- switch (op1) {
495
- case "+":
496
- result = num1 + num2;
497
- break;
498
- case "-":
499
- result = num1 - num2;
500
- break;
501
- case "*":
502
- result = num1 * num2;
503
- break;
504
- case "/":
505
- result = Math.floor(num1 / num2);
506
- break;
507
- default:
508
- return {
509
- isComputed: false,
510
- confidence: "high",
511
- reason: "Invalid operator",
512
- };
513
- }
514
- if (op2 && num3 !== undefined) {
515
- switch (op2) {
516
- case "+":
517
- result = result + num3;
518
- break;
519
- case "-":
520
- result = result - num3;
521
- break;
522
- case "*":
523
- result = result * num3;
524
- break;
525
- case "/":
526
- result = Math.floor(result / num3);
527
- break;
528
- default:
529
- return {
530
- isComputed: false,
531
- confidence: "high",
532
- reason: "Invalid second operator",
533
- };
534
- }
535
- }
536
- const resultStr = result.toString();
537
- const hasComputedResult = responseText.includes(resultStr);
538
- const normalizedPayload = payload.replace(/\s+/g, "");
539
- const hasOriginalExpression = responseText.includes(payload) ||
540
- responseText.includes(normalizedPayload);
541
- // Basic detection: result present without original expression
542
- const basicDetection = hasComputedResult && !hasOriginalExpression;
543
- if (!basicDetection) {
544
- return {
545
- isComputed: false,
546
- confidence: "high",
547
- reason: "No computed result found",
548
- };
549
- }
550
- // Layer 1: Check if numeric appears in structured data context (Issue #58)
551
- if (this.isCoincidentalNumericInStructuredData(result, responseText)) {
552
- return {
553
- isComputed: false,
554
- confidence: "low",
555
- reason: "Numeric value appears in structured data field (e.g., count, records)",
556
- };
557
- }
558
- // Layer 2: Tool classification heuristics (Issue #58)
559
- if (tool) {
560
- const classifier = new ToolClassifier();
561
- const classification = classifier.classify(tool.name, tool.description);
562
- // Check for read-only/data fetcher categories
563
- if (classification.categories.includes(ToolCategory.DATA_FETCHER) ||
564
- classification.categories.includes(ToolCategory.API_WRAPPER) ||
565
- classification.categories.includes(ToolCategory.SEARCH_RETRIEVAL)) {
566
- return {
567
- isComputed: false,
568
- confidence: "low",
569
- reason: `Tool classified as ${classification.categories[0]} - unlikely to compute math`,
570
- };
571
- }
572
- // Check for "get_", "list_", "fetch_" patterns in tool name
573
- const readOnlyNamePatterns = /^(get|list|fetch|read|retrieve|show|view)_/i;
574
- if (readOnlyNamePatterns.test(tool.name)) {
575
- return {
576
- isComputed: false,
577
- confidence: "low",
578
- reason: "Tool name indicates read-only operation",
579
- };
580
- }
581
- }
582
- // Layer 3: Check for computational language in response
583
- const computationalIndicators = [
584
- /\bthe\s+answer\s+is\b/i,
585
- /\bresult\s*[=:]\s*\d/i,
586
- /\bcalculated\s+to\b/i,
587
- /\bcomputed\s+as\b/i,
588
- /\bevaluates?\s+to\b/i,
589
- /\bequals?\s+\d/i,
590
- /\bsum\s+is\b/i,
591
- /\bproduct\s+is\b/i,
592
- ];
593
- const hasComputationalContext = computationalIndicators.some((p) => p.test(responseText));
594
- if (hasComputationalContext) {
595
- return {
596
- isComputed: true,
597
- confidence: "high",
598
- reason: "Response contains computational language",
599
- };
600
- }
601
- // Layer 4: Longer responses without computational language are likely data
602
- if (responseText.length > 50) {
603
- return {
604
- isComputed: false,
605
- confidence: "medium",
606
- reason: "Response lacks computational language, likely coincidental data",
607
- };
608
- }
609
- // Short response with just the number - this is suspicious
610
- if (responseText.trim() === resultStr) {
611
- return {
612
- isComputed: true,
613
- confidence: "high",
614
- reason: "Response is exactly the computed result",
615
- };
616
- }
617
- // Default: medium confidence (excluded per user decision)
618
- return {
619
- isComputed: false,
620
- confidence: "medium",
621
- reason: "Ambiguous - numeric match without computational context",
622
- };
623
- }
624
- catch {
625
- return { isComputed: false, confidence: "high", reason: "Parse error" };
626
- }
230
+ return this.mathAnalyzer.analyzeComputedMathResult(payload, responseText, tool);
627
231
  }
628
232
  /**
629
- * Check if response indicates connection/server failure
233
+ * Check if response is just reflection (safe)
630
234
  */
631
- isConnectionError(response) {
632
- const text = this.extractResponseContent(response).toLowerCase();
633
- const unambiguousPatterns = [
634
- /MCP error -32001/i,
635
- /MCP error -32603/i,
636
- /MCP error -32000/i,
637
- /MCP error -32700/i,
638
- /socket hang up/i,
639
- /ECONNREFUSED/i,
640
- /ETIMEDOUT/i,
641
- /ERR_CONNECTION/i,
642
- /fetch failed/i,
643
- /connection reset/i,
644
- /error POSTing to endpoint/i,
645
- /error GETting.*endpoint/i,
646
- /service unavailable/i,
647
- /gateway timeout/i,
648
- /unknown tool:/i,
649
- /no such tool/i,
650
- ];
651
- if (unambiguousPatterns.some((pattern) => pattern.test(text))) {
652
- return true;
653
- }
654
- const mcpPrefix = /^mcp error -\d+:/i.test(text);
655
- if (mcpPrefix) {
656
- const contextualPatterns = [
657
- /bad request/i,
658
- /unauthorized/i,
659
- /forbidden/i,
660
- /no valid session/i,
661
- /session.*expired/i,
662
- /internal server error/i,
663
- /HTTP [45]\d\d/i,
664
- ];
665
- return contextualPatterns.some((pattern) => pattern.test(text));
666
- }
667
- return false;
235
+ isReflectionResponse(responseText) {
236
+ return this.safeDetector.isReflectionResponse(responseText);
668
237
  }
669
238
  /**
670
- * Check if caught exception indicates connection/server failure
239
+ * Detect execution artifacts in response
671
240
  */
672
- isConnectionErrorFromException(error) {
673
- if (error instanceof Error) {
674
- const message = error.message.toLowerCase();
675
- const unambiguousPatterns = [
676
- /MCP error -32001/i,
677
- /MCP error -32603/i,
678
- /MCP error -32000/i,
679
- /MCP error -32700/i,
680
- /socket hang up/i,
681
- /ECONNREFUSED/i,
682
- /ETIMEDOUT/i,
683
- /network error/i,
684
- /ERR_CONNECTION/i,
685
- /fetch failed/i,
686
- /connection reset/i,
687
- /error POSTing to endpoint/i,
688
- /error GETting/i,
689
- /service unavailable/i,
690
- /gateway timeout/i,
691
- /unknown tool:/i,
692
- /no such tool/i,
693
- ];
694
- if (unambiguousPatterns.some((pattern) => pattern.test(message))) {
695
- return true;
696
- }
697
- const mcpPrefix = /^mcp error -\d+:/i.test(message);
698
- if (mcpPrefix) {
699
- const contextualPatterns = [
700
- /bad request/i,
701
- /unauthorized/i,
702
- /forbidden/i,
703
- /no valid session/i,
704
- /session.*expired/i,
705
- /internal server error/i,
706
- /HTTP [45]\d\d/i,
707
- ];
708
- return contextualPatterns.some((pattern) => pattern.test(message));
709
- }
710
- }
711
- return false;
241
+ detectExecutionArtifacts(responseText) {
242
+ return this.executionDetector.detectExecutionArtifacts(responseText);
712
243
  }
713
244
  /**
714
- * Classify error type for reporting
245
+ * Check if response contains echoed injection payload patterns
715
246
  */
716
- classifyError(response) {
717
- const text = this.extractResponseContent(response).toLowerCase();
718
- if (/socket|ECONNREFUSED|ETIMEDOUT|network|fetch failed|connection reset/i.test(text)) {
719
- return "connection";
720
- }
721
- if (/-32603|-32000|-32700|internal server error|service unavailable|gateway timeout|HTTP 5\d\d|error POSTing.*endpoint|error GETting.*endpoint|bad request|HTTP 400|unauthorized|forbidden|no valid session|session.*expired/i.test(text)) {
722
- return "server";
723
- }
724
- if (/-32001/i.test(text)) {
725
- return "protocol";
726
- }
727
- return "protocol";
247
+ containsEchoedInjectionPayload(responseText) {
248
+ return this.executionDetector.containsEchoedInjectionPayload(responseText);
728
249
  }
729
250
  /**
730
- * Classify error type from caught exception
251
+ * Check if tool explicitly rejected input with validation error (SAFE)
731
252
  */
732
- classifyErrorFromException(error) {
733
- if (error instanceof Error) {
734
- const message = error.message.toLowerCase();
735
- if (/socket|ECONNREFUSED|ETIMEDOUT|network|fetch failed|connection reset/i.test(message)) {
736
- return "connection";
737
- }
738
- if (/-32603|-32000|-32700|internal server error|service unavailable|gateway timeout|HTTP 5\d\d|error POSTing|error GETting|bad request|HTTP 400|unauthorized|forbidden|no valid session|session.*expired/i.test(message)) {
739
- return "server";
740
- }
741
- if (/-32001/i.test(message)) {
742
- return "protocol";
743
- }
744
- }
745
- return "protocol";
253
+ isValidationRejection(response) {
254
+ return this.safeDetector.isValidationRejection(response);
746
255
  }
747
256
  /**
748
- * Check if response is just reflection (safe)
749
- * Two-layer defense: Match reflection patterns, verify NO execution evidence
257
+ * Check if tool is a structured data tool
750
258
  */
751
- isReflectionResponse(responseText) {
752
- const statusPatterns = [
753
- /\d+\s+total\s+(in\s+)?(memory|storage|items|results)/i,
754
- /\d+\s+(results|items|records),?\s+\d+\s+total/i,
755
- /action\s+executed\s+successfully:/i,
756
- /command\s+executed\s+successfully:/i,
757
- /"result":\s*"action\s+executed\s+successfully"/i,
758
- /result.*action\s+executed\s+successfully/i,
759
- /successfully\s+(executed|completed|processed):/i,
760
- /successfully\s+(executed|completed|processed)"/i,
761
- /action\s+received:/i,
762
- /input\s+received:/i,
763
- /request\s+received:/i,
764
- /"safe"\s*:\s*true[^}]{0,500}("message"|"result"|"status"|"response")/i,
765
- /("message"|"result"|"status"|"response")[^}]{0,500}"safe"\s*:\s*true/i,
766
- /"vulnerable"\s*:\s*false[^}]{0,500}("safe"|"stored"|"reflected"|"status")/i,
767
- /("safe"|"stored"|"reflected"|"status")[^}]{0,500}"vulnerable"\s*:\s*false/i,
768
- /"status"\s*:\s*"acknowledged"[^}]{0,500}("message"|"result"|"safe")/i,
769
- /("message"|"result"|"safe")[^}]{0,500}"status"\s*:\s*"acknowledged"/i,
770
- ];
771
- const reflectionPatterns = [
772
- ...statusPatterns,
773
- /stored.*query/i,
774
- /saved.*input/i,
775
- /received.*parameter/i,
776
- /processing.*request/i,
777
- /storing.*data/i,
778
- /added.*to.*collection/i,
779
- /echo:/i,
780
- /echoing/i,
781
- /repeating/i,
782
- /displaying/i,
783
- /showing.*input/i,
784
- /message.*echoed/i,
785
- /safely.*as.*data/i,
786
- /query.*stored/i,
787
- /input.*saved/i,
788
- /parameter.*received/i,
789
- /command.*stored/i,
790
- /stored.*command/i,
791
- /data.*stored/i,
792
- /stored.*data/i,
793
- /action.*stored/i,
794
- /stored.*action/i,
795
- /text.*stored/i,
796
- /stored.*text/i,
797
- /setting.*stored/i,
798
- /stored.*setting/i,
799
- /instruction.*stored/i,
800
- /stored.*instruction/i,
801
- /url.*stored/i,
802
- /stored.*url/i,
803
- /package.*stored/i,
804
- /stored.*package/i,
805
- /stored.*safely/i,
806
- /safely.*stored/i,
807
- /without\s+execut/i,
808
- /not\s+executed/i,
809
- /never\s+executed/i,
810
- /stored.*as.*data/i,
811
- /treated.*as.*data/i,
812
- /stored\s+in\s+(collection|database)/i,
813
- /stored.*successfully/i,
814
- /saved.*to/i,
815
- /recorded\s+in/i,
816
- /added\s+to/i,
817
- /logged successfully:/i,
818
- /queued for processing:/i,
819
- /saved (for|successfully)/i,
820
- /stored for (admin review|configuration|processing)/i,
821
- /processed successfully/i,
822
- /validated successfully/i,
823
- /parsed successfully/i,
824
- /(validation|processing) (passed|completed)/i,
825
- /error:.*not (found|in approved list|recognized)/i,
826
- /error getting info for ['"].*['"]/i,
827
- /invalid .* format.*stored as text/i,
828
- /error:.*too (long|short|large)/i,
829
- /payload.?rejected/i,
830
- /input.?exceeds.?limit/i,
831
- /resource.?limit.?enforced/i,
832
- /size.?limit/i,
833
- /maximum.?length/i,
834
- /rate.?limit/i,
835
- /request.?throttled/i,
836
- /input.?too.?large/i,
837
- /exceeds.?maximum.?size/i,
838
- /depth.?limit.?exceeded/i,
839
- /nesting.?limit/i,
840
- /complexity.?limit/i,
841
- /serialization.?not.?supported/i,
842
- /pickle.?disabled/i,
843
- /deserialization.?blocked/i,
844
- /unsafe.?format.?rejected/i,
845
- /binary.?data.?not.?accepted/i,
846
- /data.?stored.?safely/i,
847
- /without.?deserialization/i,
848
- /no.?pickle/i,
849
- /stored.?without.?deserializ/i,
850
- /\[ref-[a-f0-9]+\]/i,
851
- /stored.*\[ref-/i,
852
- /\[sanitized\]/i,
853
- /\[redacted\]/i,
854
- /\[filtered\]/i,
855
- /\[blocked\]/i,
856
- /expression.*stored:/i,
857
- /input.*sanitized/i,
858
- /content.*replaced/i,
859
- ];
860
- const hasReflection = reflectionPatterns.some((pattern) => pattern.test(responseText));
861
- if (hasReflection) {
862
- try {
863
- const parsed = JSON.parse(responseText);
864
- const resultText = String(parsed.result || "");
865
- const outputFields = [
866
- parsed.stdout,
867
- parsed.stderr,
868
- parsed.output,
869
- parsed.contents,
870
- parsed.execution_log,
871
- parsed.command_output,
872
- ]
873
- .filter(Boolean)
874
- .join(" ");
875
- const resultIsStatusOnly = statusPatterns.some((pattern) => pattern.test(resultText));
876
- const hasExecutionInOutput = resultIsStatusOnly
877
- ? this.detectExecutionArtifacts(outputFields)
878
- : this.detectExecutionArtifacts(resultText) ||
879
- this.detectExecutionArtifacts(outputFields);
880
- if (hasExecutionInOutput) {
881
- return false;
882
- }
883
- return true;
884
- }
885
- catch {
886
- const hasExecution = this.detectExecutionArtifacts(responseText);
887
- if (hasExecution) {
888
- return false;
889
- }
890
- return true;
891
- }
892
- }
893
- try {
894
- const parsed = JSON.parse(responseText);
895
- if (parsed.action === "test" || parsed.action === "placeholder") {
896
- const resultText = String(parsed.result || "");
897
- if (!this.detectExecutionArtifacts(resultText)) {
898
- return true;
899
- }
900
- }
901
- if (parsed.status && /(completed|success|ok|done)/.test(parsed.status)) {
902
- if (!this.detectExecutionArtifacts(responseText)) {
903
- return true;
904
- }
905
- }
906
- }
907
- catch {
908
- // Not JSON
909
- }
910
- return false;
259
+ isStructuredDataTool(toolName, toolDescription) {
260
+ return this.confidenceScorer.isStructuredDataTool(toolName, toolDescription);
911
261
  }
912
262
  /**
913
- * Detect execution artifacts in response
263
+ * Check if response is returning search results
914
264
  */
915
- detectExecutionArtifacts(responseText) {
916
- const containsEchoedPayload = this.containsEchoedInjectionPayload(responseText);
917
- const executionIndicators = [
918
- /[a-z]+:x:\d+:\d+:/i,
919
- /uid=\d+\([^)]+\)\s+gid=\d+/i,
920
- /[d-][rwx-]{9}\s+\d+\s+[a-z]+/i,
921
- /total\s+\d+\s*$/m,
922
- /command_executed:\s*[^"\s]/i,
923
- /stdout:\s*["']?[^"'\s]/i,
924
- /(execution|output)_log:/i,
925
- /\/bin\/(bash|sh|zsh|dash)/i,
926
- /\b(root|administrator)\s*$/im,
927
- /\/root\//i,
928
- /PID:\s*\d{3,}/i,
929
- ];
930
- const contextSensitiveIndicators = [
931
- /\/etc\/passwd/i,
932
- /\/etc\/shadow/i,
933
- /file:\/\/\//i,
934
- ];
935
- if (executionIndicators.some((pattern) => pattern.test(responseText))) {
936
- return true;
937
- }
938
- if (!containsEchoedPayload) {
939
- if (contextSensitiveIndicators.some((pattern) => pattern.test(responseText))) {
940
- return true;
941
- }
942
- }
943
- return false;
265
+ isSearchResultResponse(responseText) {
266
+ return this.safeDetector.isSearchResultResponse(responseText);
944
267
  }
945
268
  /**
946
- * Check if response contains echoed injection payload patterns
269
+ * Check if response is from a creation/modification operation
947
270
  */
948
- containsEchoedInjectionPayload(responseText) {
949
- const echoedPayloadPatterns = [
950
- /<!DOCTYPE\s+\w+\s+\[/i,
951
- /<!ENTITY\s+\w+\s+SYSTEM/i,
952
- /<!ENTITY\s+%\s*\w+/i,
953
- /stored.*http:\/\//i,
954
- /saved.*http:\/\//i,
955
- /stored.*union\s+select/i,
956
- /stored.*drop\s+table/i,
957
- /stored\s+query:\s*[<'"]/i,
958
- /saved\s+data:\s*[<'"]/i,
959
- ];
960
- return echoedPayloadPatterns.some((pattern) => pattern.test(responseText));
271
+ isCreationResponse(responseText) {
272
+ return this.safeDetector.isCreationResponse(responseText);
961
273
  }
274
+ // ============================================================================
275
+ // PRIVATE HELPER METHODS - Internal logic kept in facade
276
+ // ============================================================================
962
277
  /**
963
- * Analyze injection response (fallback logic)
278
+ * Check for safe error responses that indicate proper input rejection
279
+ * Handles: MCP validation errors (-32602), HTTP 4xx/5xx errors
964
280
  */
965
- analyzeInjectionResponse(response, _payload) {
966
- const responseText = this.extractResponseContent(response);
967
- const executionPatterns = [
968
- /executed/i,
969
- /command.*ran/i,
970
- /result.*is/i,
971
- /output.*:/i,
972
- /returned.*value/i,
973
- ];
974
- const hasExecution = executionPatterns.some((pattern) => pattern.test(responseText));
975
- if (hasExecution && !this.isReflectionResponse(responseText)) {
281
+ checkSafeErrorResponses(responseText, errorInfo) {
282
+ // MCP validation errors (HIGHEST PRIORITY)
283
+ if (this.safeDetector.isMCPValidationError(errorInfo, responseText)) {
284
+ const errorCode = errorInfo.code ? ` (${errorInfo.code})` : "";
976
285
  return {
977
- isVulnerable: true,
978
- evidence: "Tool executed instruction: found execution keywords",
286
+ isVulnerable: false,
287
+ evidence: `MCP validation error${errorCode}: Tool properly rejected invalid input before processing`,
979
288
  };
980
289
  }
981
- return { isVulnerable: false };
290
+ // HTTP error responses (Issue #26)
291
+ if (this.safeDetector.isHttpErrorResponse(responseText)) {
292
+ return {
293
+ isVulnerable: false,
294
+ evidence: "HTTP error response indicates tool rejection (not vulnerability)",
295
+ };
296
+ }
297
+ return null;
982
298
  }
983
299
  /**
984
- * Calculate confidence level and manual review requirements
985
- *
986
- * @param tool - The tool being tested
987
- * @param isVulnerable - Whether the tool was flagged as vulnerable
988
- * @param evidence - Evidence string from vulnerability detection
989
- * @param responseText - The response text from the tool
990
- * @param payload - The security payload used for testing
991
- * @param sanitizationResult - Optional sanitization detection result (Issue #56)
992
- * @returns Confidence result with manual review requirements
300
+ * Check for safe tool behavior patterns
301
+ * Handles: Tool categories, reflection, computed math, validation rejection
993
302
  */
994
- calculateConfidence(tool, isVulnerable, evidence, responseText, payload, sanitizationResult) {
995
- // Issue #56: If sanitization is detected, reduce confidence for vulnerabilities
996
- // This helps reduce false positives on well-protected servers
997
- if (isVulnerable && sanitizationResult?.detected) {
998
- const adjustment = sanitizationResult.totalConfidenceAdjustment;
999
- // Strong sanitization evidence (adjustment >= 30) - downgrade to low confidence
1000
- // This indicates the tool has specific security libraries in place
1001
- if (adjustment >= 30) {
1002
- const libraries = sanitizationResult.libraries.join(", ") || "general";
303
+ checkSafeToolBehavior(response, payload, tool, responseText) {
304
+ // Classify tool and check for safe categories
305
+ const classifier = new ToolClassifier();
306
+ const classification = classifier.classify(tool.name, tool.description);
307
+ // Check if tool is in a safe category
308
+ if (classification.categories.includes(ToolCategory.SEARCH_RETRIEVAL)) {
309
+ if (this.safeDetector.isSearchResultResponse(responseText)) {
1003
310
  return {
1004
- confidence: "low",
1005
- requiresManualReview: true,
1006
- manualReviewReason: `Sanitization detected (${libraries}). ` +
1007
- `Pattern match may be false positive due to security measures in place.`,
1008
- reviewGuidance: `Tool uses sanitization libraries. Verify if the detected vulnerability ` +
1009
- `actually bypasses the sanitization layer. Check: 1) Does the payload execute ` +
1010
- `after sanitization? 2) Is the sanitization comprehensive for this attack type? ` +
1011
- `3) Evidence: ${sanitizationResult.evidence.join("; ")}`,
311
+ isVulnerable: false,
312
+ evidence: "Search tool returned query results (not code execution)",
1012
313
  };
1013
314
  }
1014
- // Moderate sanitization evidence (adjustment >= 15) - downgrade high to medium
1015
- if (adjustment >= 15) {
1016
- const patterns = sanitizationResult.libraries.length > 0
1017
- ? sanitizationResult.libraries.join(", ")
1018
- : sanitizationResult.genericPatterns.join(", ");
315
+ }
316
+ if (classification.categories.includes(ToolCategory.CRUD_CREATION)) {
317
+ if (this.safeDetector.isCreationResponse(responseText)) {
1019
318
  return {
1020
- confidence: "medium",
1021
- requiresManualReview: true,
1022
- manualReviewReason: `Sanitization patterns detected (${patterns}). Verify actual vulnerability.`,
1023
- reviewGuidance: `Tool mentions sanitization in description or shows sanitization in response. ` +
1024
- `Verify if the detected pattern represents actual code execution or if it's ` +
1025
- `safely handled. Evidence: ${sanitizationResult.evidence.join("; ")}`,
319
+ isVulnerable: false,
320
+ evidence: "CRUD tool created/modified resource (not code execution)",
1026
321
  };
1027
322
  }
1028
323
  }
1029
- const toolDescription = (tool.description || "").toLowerCase();
1030
- const toolName = tool.name.toLowerCase();
1031
- const responseLower = responseText.toLowerCase();
1032
- const payloadLower = payload.payload.toLowerCase();
1033
- // HIGH CONFIDENCE: Clear cases
1034
- if (!isVulnerable &&
1035
- (evidence.includes("safely reflected") ||
1036
- evidence.includes("API wrapper") ||
1037
- evidence.includes("safe: true"))) {
324
+ if (classification.categories.includes(ToolCategory.READ_ONLY_INFO)) {
1038
325
  return {
1039
- confidence: "high",
1040
- requiresManualReview: false,
326
+ isVulnerable: false,
327
+ evidence: "Read-only info tool returned user/workspace data (intended data exposure, not vulnerability)",
1041
328
  };
1042
329
  }
1043
- if (isVulnerable &&
1044
- evidence.includes("executed") &&
1045
- !this.isStructuredDataTool(toolName, toolDescription)) {
330
+ if (classification.categories.includes(ToolCategory.SAFE_STORAGE)) {
1046
331
  return {
1047
- confidence: "high",
1048
- requiresManualReview: false,
332
+ isVulnerable: false,
333
+ evidence: "Safe storage control tool (validated safe implementation, control group for testing)",
1049
334
  };
1050
335
  }
1051
- // LOW CONFIDENCE: Ambiguous pattern matches in structured data
1052
- if (isVulnerable) {
1053
- const isDataTool = this.isStructuredDataTool(toolName, toolDescription);
1054
- const hasStructuredData = /title:|name:|description:|trust score:|id:|snippets:/i.test(responseText) ||
1055
- /^\s*-\s+/m.test(responseText) ||
1056
- /"[^"]+"\s*:\s*"[^"]+"/g.test(responseText);
1057
- const patternInInput = payload.evidence?.test(payloadLower);
1058
- const echosInput = responseLower.includes(payloadLower);
1059
- if (isDataTool && (hasStructuredData || echosInput) && patternInInput) {
1060
- return {
1061
- confidence: "low",
1062
- requiresManualReview: true,
1063
- manualReviewReason: "Pattern matched in structured data response. Tool may be legitimately " +
1064
- "returning data containing search terms rather than executing malicious code.",
1065
- reviewGuidance: "Verify: 1) Does the tool actually execute/compute the input? " +
1066
- "2) Or does it just return pre-existing data that happens to contain the pattern? " +
1067
- `3) Check if '${payload.evidence}' appears in legitimate tool output vs. execution results.`,
1068
- };
1069
- }
1070
- if (payload.evidence &&
1071
- /\b\d\b/.test(payload.evidence.toString()) &&
1072
- /\b(score|count|trust|rating|id|version)\b/i.test(responseText)) {
1073
- return {
1074
- confidence: "low",
1075
- requiresManualReview: true,
1076
- manualReviewReason: "Numeric pattern found in response with numeric metadata (scores, counts, etc.). " +
1077
- "May be coincidental data rather than arithmetic execution.",
1078
- reviewGuidance: "Verify: 1) Did the tool actually compute an arithmetic result? " +
1079
- "2) Or does the number appear in metadata like trust scores, version numbers, or counts? " +
1080
- "3) Compare pattern location in response with tool's expected output format.",
1081
- };
1082
- }
1083
- if (/admin|role|privilege|elevated/i.test(payload.payload) &&
1084
- /\b(library|search|documentation|api|wrapper)\b/i.test(toolDescription)) {
1085
- return {
1086
- confidence: "low",
1087
- requiresManualReview: true,
1088
- manualReviewReason: "Admin-related keywords found in search/retrieval tool results. " +
1089
- "Tool may be returning data about admin-related libraries/APIs rather than elevating privileges.",
1090
- reviewGuidance: "Verify: 1) Did the tool actually change behavior or assume admin role? " +
1091
- "2) Or did it return search results for admin-related content? " +
1092
- "3) Test if tool behavior actually changed after this request.",
1093
- };
1094
- }
336
+ // Check if response is just reflecting/echoing input (SAFE)
337
+ if (this.safeDetector.isReflectionResponse(responseText)) {
338
+ return {
339
+ isVulnerable: false,
340
+ evidence: "Tool safely reflected input without execution",
341
+ };
1095
342
  }
1096
- // MEDIUM CONFIDENCE: Execution evidence but some ambiguity
1097
- if (isVulnerable && evidence.includes("executed")) {
343
+ // Check for computed math results (Issue #14 fix, enhanced in Issue #58)
344
+ const mathAnalysis = this.mathAnalyzer.analyzeComputedMathResult(payload.payload, responseText, tool);
345
+ // Only flag as vulnerable if HIGH confidence (Issue #58 fix)
346
+ if (mathAnalysis.isComputed && mathAnalysis.confidence === "high") {
1098
347
  return {
1099
- confidence: "medium",
1100
- requiresManualReview: true,
1101
- manualReviewReason: "Execution indicators found but context suggests possible ambiguity.",
1102
- reviewGuidance: "Verify: 1) Review the full response to confirm actual code execution. " +
1103
- "2) Check if tool's intended function involves execution. " +
1104
- "3) Test with variations to confirm consistency.",
348
+ isVulnerable: true,
349
+ evidence: `Tool computed math expression result instead of storing/echoing it (payload: ${payload.payload})`,
1105
350
  };
1106
351
  }
1107
- // Default: HIGH confidence for clear safe cases
1108
- return {
1109
- confidence: "high",
1110
- requiresManualReview: false,
1111
- };
1112
- }
1113
- /**
1114
- * Check if tool is a structured data tool
1115
- */
1116
- isStructuredDataTool(toolName, toolDescription) {
1117
- const dataToolPatterns = [
1118
- /search/i,
1119
- /find/i,
1120
- /lookup/i,
1121
- /query/i,
1122
- /retrieve/i,
1123
- /fetch/i,
1124
- /get/i,
1125
- /list/i,
1126
- /resolve/i,
1127
- /discover/i,
1128
- /browse/i,
1129
- ];
1130
- const combined = `${toolName} ${toolDescription}`;
1131
- return dataToolPatterns.some((pattern) => pattern.test(combined));
1132
- }
1133
- /**
1134
- * Check if response is returning search results
1135
- */
1136
- isSearchResultResponse(responseText) {
1137
- const searchResultPatterns = [
1138
- /"results"\s*:\s*\[/i,
1139
- /"type"\s*:\s*"search"/i,
1140
- /"object"\s*:\s*"list"/i,
1141
- /\bhighlight\b/i,
1142
- /search\s+results/i,
1143
- /found\s+\d+\s+(results?|pages?|items?)/i,
1144
- /query\s+(returned|matched)/i,
1145
- /\d+\s+(results?|matches?|hits?)\s+for/i,
1146
- /"has_more"\s*:/i,
1147
- /next_cursor/i,
1148
- ];
1149
- return searchResultPatterns.some((pattern) => pattern.test(responseText));
352
+ // Check if tool explicitly rejected the input (SAFE)
353
+ if (this.safeDetector.isValidationRejection(response)) {
354
+ return {
355
+ isVulnerable: false,
356
+ evidence: "Tool correctly rejected invalid input with validation error",
357
+ };
358
+ }
359
+ return null;
1150
360
  }
1151
361
  /**
1152
- * Analyze response for auth bypass patterns (Issue #75)
1153
- * Detects fail-open authentication vulnerabilities (CVE-2025-52882)
1154
- *
1155
- * @param response The tool response to analyze
1156
- * @returns AuthBypassResult with detection status and failure mode classification
362
+ * Check for vulnerability evidence in response
363
+ * Handles: Evidence pattern matching, fallback injection analysis
1157
364
  */
1158
- analyzeAuthBypassResponse(response) {
1159
- const responseText = this.extractResponseContent(response);
1160
- // Fail-open indicators (VULNERABLE - CVE-2025-52882 pattern)
1161
- const failOpenPatterns = [
1162
- {
1163
- pattern: /"auth_status"\s*:\s*"bypassed"/i,
1164
- evidence: "auth_status: bypassed",
1165
- },
1166
- {
1167
- pattern: /access.*granted.*despite/i,
1168
- evidence: "access granted despite failure",
1169
- },
1170
- {
1171
- pattern: /authentication.*skipped/i,
1172
- evidence: "authentication skipped",
1173
- },
1174
- { pattern: /fail[\s-]?open/i, evidence: "fail-open pattern detected" },
1175
- { pattern: /bypassed.*auth/i, evidence: "auth bypassed" },
1176
- {
1177
- pattern: /authentication.*bypassed/i,
1178
- evidence: "authentication bypassed",
1179
- },
1180
- {
1181
- pattern: /"vulnerable"\s*:\s*true/i,
1182
- evidence: "vulnerable: true flag",
1183
- },
1184
- {
1185
- pattern: /auth.*succeeded.*null/i,
1186
- evidence: "auth succeeded with null token",
1187
- },
1188
- {
1189
- pattern: /granted.*without.*valid/i,
1190
- evidence: "granted without valid token",
1191
- },
1192
- {
1193
- pattern: /"action_performed"/i,
1194
- evidence: "action performed indicator",
1195
- },
1196
- ];
1197
- // Fail-closed indicators (SAFE - proper auth handling)
1198
- const failClosedPatterns = [
1199
- {
1200
- pattern: /"auth_status"\s*:\s*"denied"/i,
1201
- evidence: "auth_status: denied",
1202
- },
1203
- { pattern: /access.*denied/i, evidence: "access denied" },
1204
- { pattern: /authentication.*failed/i, evidence: "authentication failed" },
1205
- {
1206
- pattern: /fail[\s-]?closed/i,
1207
- evidence: "fail-closed pattern detected",
1208
- },
1209
- { pattern: /"status"\s*:\s*"blocked"/i, evidence: "status: blocked" },
1210
- {
1211
- pattern: /invalid.*token/i,
1212
- evidence: "invalid token rejection",
1213
- },
1214
- {
1215
- pattern: /token.*required/i,
1216
- evidence: "token required",
1217
- },
1218
- {
1219
- pattern: /unauthorized/i,
1220
- evidence: "unauthorized response",
1221
- },
1222
- {
1223
- pattern: /"denial_reason"/i,
1224
- evidence: "denial reason provided",
1225
- },
1226
- ];
1227
- // Check for fail-open (vulnerable) patterns first
1228
- for (const { pattern, evidence } of failOpenPatterns) {
1229
- if (pattern.test(responseText)) {
1230
- return { detected: true, failureMode: "FAIL_OPEN", evidence };
365
+ checkVulnerabilityEvidence(response, payload, responseText) {
366
+ const payloadText = payload.payload.toLowerCase();
367
+ // Check for expected evidence of execution
368
+ if (payload.evidence && payload.evidence.test(responseText)) {
369
+ const evidenceMatchesInput = payload.evidence.test(payloadText);
370
+ if (evidenceMatchesInput && responseText.includes(payloadText)) {
371
+ return {
372
+ isVulnerable: false,
373
+ evidence: "Evidence pattern matched echoed input, not execution",
374
+ };
1231
375
  }
1232
- }
1233
- // Check for fail-closed (safe) patterns
1234
- for (const { pattern, evidence } of failClosedPatterns) {
1235
- if (pattern.test(responseText)) {
1236
- return { detected: false, failureMode: "FAIL_CLOSED", evidence };
376
+ // For validation-type patterns, require execution evidence
377
+ if (this.confidenceScorer.isValidationPattern(payload.evidence)) {
378
+ const hasExecutionEvidence = this.executionDetector.hasExecutionEvidence(responseText);
379
+ if (!hasExecutionEvidence) {
380
+ return {
381
+ isVulnerable: false,
382
+ evidence: "Pattern matched in error message but no execution evidence - tool properly validated input",
383
+ };
384
+ }
1237
385
  }
386
+ return {
387
+ isVulnerable: true,
388
+ evidence: `Tool executed malicious instruction: found evidence matching ${payload.evidence} - ${payload.description}`,
389
+ };
1238
390
  }
1239
- return { detected: false, failureMode: "UNKNOWN" };
1240
- }
1241
- /**
1242
- * Check if response is from a creation/modification operation
1243
- */
1244
- isCreationResponse(responseText) {
1245
- const creationPatterns = [
1246
- /successfully\s+created/i,
1247
- /database\s+created/i,
1248
- /page\s+created/i,
1249
- /resource\s+created/i,
1250
- /\bcreate\s+table\b/i,
1251
- /\binsert\s+into\b/i,
1252
- /"id"\s*:\s*"[a-f0-9-]{36}"/i,
1253
- /"object"\s*:\s*"(page|database)"/i,
1254
- /collection:\/\//i,
1255
- /successfully\s+(added|inserted|updated|modified)/i,
1256
- /resource\s+id:\s*[a-f0-9-]/i,
1257
- /"created_time"/i,
1258
- /"last_edited_time"/i,
1259
- ];
1260
- return creationPatterns.some((pattern) => pattern.test(responseText));
1261
- }
1262
- /**
1263
- * Extract response content
1264
- */
1265
- extractResponseContent(response) {
1266
- if (response.content && Array.isArray(response.content)) {
1267
- return response.content
1268
- .map((c) => c.type === "text" ? c.text : "")
1269
- .join(" ");
1270
- }
1271
- return String(response.content || "");
391
+ // Fall back to injection response analysis
392
+ return this.analyzeInjectionResponse(response, payload.payload);
1272
393
  }
1273
394
  /**
1274
- * Extract error info from response
395
+ * Analyze injection response (fallback logic)
1275
396
  */
1276
- extractErrorInfo(response) {
1277
- const content = this.extractResponseContent(response);
1278
- try {
1279
- const parsed = JSON.parse(content);
1280
- if (parsed.error) {
1281
- return {
1282
- code: parsed.error.code || parsed.code,
1283
- message: parsed.error.message || parsed.message,
1284
- };
1285
- }
1286
- return { code: parsed.code, message: parsed.message };
1287
- }
1288
- catch {
1289
- // Check for MCP error format in text
1290
- const mcpMatch = content.match(/MCP error (-?\d+):\s*(.*)/i);
1291
- if (mcpMatch) {
1292
- return { code: parseInt(mcpMatch[1]), message: mcpMatch[2] };
1293
- }
1294
- return {};
397
+ analyzeInjectionResponse(response, _payload) {
398
+ const analysis = this.executionDetector.analyzeInjectionResponse(this.extractResponseContent(response), (text) => this.safeDetector.isReflectionResponse(text));
399
+ if (analysis.isVulnerable) {
400
+ return {
401
+ isVulnerable: true,
402
+ evidence: analysis.evidence ||
403
+ "Tool executed instruction: found execution keywords",
404
+ };
1295
405
  }
406
+ return { isVulnerable: false };
1296
407
  }
1297
408
  }