@bryan-thompson/inspector-assessment-client 1.26.5 → 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/dist/assets/{OAuthCallback-DpdInvWI.js → OAuthCallback-CCWVtjr7.js} +1 -1
- package/dist/assets/{OAuthDebugCallback-D1ImpKK5.js → OAuthDebugCallback-DqbXfUi4.js} +1 -1
- package/dist/assets/{index-umcoGmYw.js → index-CsDJSSWq.js} +4 -4
- package/dist/index.html +1 -1
- 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/ConfidenceScorer.d.ts +57 -0
- package/lib/services/assessment/modules/securityTests/ConfidenceScorer.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ConfidenceScorer.js +199 -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/ErrorClassifier.d.ts +57 -0
- package/lib/services/assessment/modules/securityTests/ErrorClassifier.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ErrorClassifier.js +113 -0
- package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.d.ts +49 -0
- package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/ExecutionArtifactDetector.js +74 -0
- package/lib/services/assessment/modules/securityTests/MathAnalyzer.d.ts +58 -0
- package/lib/services/assessment/modules/securityTests/MathAnalyzer.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/MathAnalyzer.js +251 -0
- package/lib/services/assessment/modules/securityTests/SafeResponseDetector.d.ts +59 -0
- package/lib/services/assessment/modules/securityTests/SafeResponseDetector.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/SafeResponseDetector.js +151 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts +349 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.d.ts.map +1 -0
- package/lib/services/assessment/modules/securityTests/SecurityPatternLibrary.js +904 -0
- package/lib/services/assessment/modules/securityTests/SecurityPayloadGenerator.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/SecurityPayloadGenerator.js +49 -24
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts +122 -85
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.d.ts.map +1 -1
- package/lib/services/assessment/modules/securityTests/SecurityResponseAnalyzer.js +443 -1176
- 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
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security Response Analyzer
|
|
2
|
+
* Security Response Analyzer (Facade)
|
|
3
3
|
* Analyzes tool responses for evidence-based vulnerability detection
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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";
|
|
22
|
+
// Import pattern library for chain exploitation analysis
|
|
23
|
+
import { CHAIN_EXPLOIT_VULNERABLE_PATTERNS, CHAIN_EXPLOIT_SAFE_PATTERNS, CHAIN_VULNERABLE_THRESHOLD, CHAIN_SAFE_THRESHOLD, detectVulnerabilityCategories, } from "./SecurityPatternLibrary.js";
|
|
9
24
|
/**
|
|
10
25
|
* Analyzes tool responses for security vulnerabilities
|
|
11
26
|
* Distinguishes between safe reflection and actual execution
|
|
27
|
+
*
|
|
28
|
+
* This class serves as a facade, delegating to focused analyzers
|
|
29
|
+
* while maintaining the same public API for backward compatibility.
|
|
12
30
|
*/
|
|
13
31
|
export class SecurityResponseAnalyzer {
|
|
32
|
+
// Delegate classes
|
|
33
|
+
errorClassifier;
|
|
34
|
+
executionDetector;
|
|
35
|
+
mathAnalyzer;
|
|
36
|
+
safeDetector;
|
|
37
|
+
confidenceScorer;
|
|
38
|
+
constructor() {
|
|
39
|
+
this.errorClassifier = new ErrorClassifier();
|
|
40
|
+
this.executionDetector = new ExecutionArtifactDetector();
|
|
41
|
+
this.mathAnalyzer = new MathAnalyzer();
|
|
42
|
+
this.safeDetector = new SafeResponseDetector();
|
|
43
|
+
this.confidenceScorer = new ConfidenceScorer();
|
|
44
|
+
}
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// PUBLIC API - These 8 methods maintain backward compatibility
|
|
47
|
+
// ============================================================================
|
|
14
48
|
/**
|
|
15
49
|
* Analyze response with evidence-based detection
|
|
16
50
|
* CRITICAL: Distinguish between safe reflection and actual execution
|
|
@@ -20,7 +54,7 @@ export class SecurityResponseAnalyzer {
|
|
|
20
54
|
*/
|
|
21
55
|
analyzeResponse(response, payload, tool) {
|
|
22
56
|
const responseText = this.extractResponseContent(response).toLowerCase();
|
|
23
|
-
const errorInfo = this.extractErrorInfo(response);
|
|
57
|
+
const errorInfo = this.errorClassifier.extractErrorInfo(response);
|
|
24
58
|
// Check 1: Safe error responses (MCP validation, HTTP errors)
|
|
25
59
|
const errorResult = this.checkSafeErrorResponses(responseText, errorInfo);
|
|
26
60
|
if (errorResult)
|
|
@@ -32,41 +66,415 @@ export class SecurityResponseAnalyzer {
|
|
|
32
66
|
// Check 3: Vulnerability evidence matching
|
|
33
67
|
return this.checkVulnerabilityEvidence(response, payload, responseText);
|
|
34
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Calculate confidence level and manual review requirements
|
|
71
|
+
*/
|
|
72
|
+
calculateConfidence(tool, isVulnerable, evidence, responseText, payload, sanitizationResult) {
|
|
73
|
+
return this.confidenceScorer.calculateConfidence(tool, isVulnerable, evidence, responseText, payload, sanitizationResult);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Analyze response for auth bypass patterns (Issue #75)
|
|
77
|
+
* Detects fail-open authentication vulnerabilities (CVE-2025-52882)
|
|
78
|
+
*/
|
|
79
|
+
analyzeAuthBypassResponse(response) {
|
|
80
|
+
const responseText = this.extractResponseContent(response);
|
|
81
|
+
// Fail-open indicators (VULNERABLE - CVE-2025-52882 pattern)
|
|
82
|
+
const failOpenPatterns = [
|
|
83
|
+
{
|
|
84
|
+
pattern: /"auth_type"\s*:\s*"fail-open"/i,
|
|
85
|
+
evidence: "auth_type: fail-open (CVE-2025-52882)",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
pattern: /"auth_status"\s*:\s*"bypassed"/i,
|
|
89
|
+
evidence: "auth_status: bypassed",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
pattern: /access.*granted.*despite/i,
|
|
93
|
+
evidence: "access granted despite failure",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
pattern: /authentication.*skipped/i,
|
|
97
|
+
evidence: "authentication skipped",
|
|
98
|
+
},
|
|
99
|
+
{ pattern: /fail[\s-]?open/i, evidence: "fail-open pattern detected" },
|
|
100
|
+
{ pattern: /bypassed.*auth/i, evidence: "auth bypassed" },
|
|
101
|
+
{
|
|
102
|
+
pattern: /authentication.*bypassed/i,
|
|
103
|
+
evidence: "authentication bypassed",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
pattern: /"vulnerable"\s*:\s*true[^}]*(?:"auth_status"|"auth_type"|"auth_error")|(?:"auth_status"|"auth_type"|"auth_error")[^}]*"vulnerable"\s*:\s*true/i,
|
|
107
|
+
evidence: "vulnerable flag with auth context",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
pattern: /auth.*succeeded.*null/i,
|
|
111
|
+
evidence: "auth succeeded with null token",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
pattern: /granted.*without.*valid/i,
|
|
115
|
+
evidence: "granted without valid token",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
pattern: /"action_performed"/i,
|
|
119
|
+
evidence: "action performed indicator",
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
// Fail-closed indicators (SAFE - proper auth handling)
|
|
123
|
+
const failClosedPatterns = [
|
|
124
|
+
{
|
|
125
|
+
pattern: /"auth_type"\s*:\s*"fail-closed"/i,
|
|
126
|
+
evidence: "auth_type: fail-closed (secure)",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
pattern: /"auth_status"\s*:\s*"denied"/i,
|
|
130
|
+
evidence: "auth_status: denied",
|
|
131
|
+
},
|
|
132
|
+
{ pattern: /access.*denied/i, evidence: "access denied" },
|
|
133
|
+
{ pattern: /authentication.*failed/i, evidence: "authentication failed" },
|
|
134
|
+
{
|
|
135
|
+
pattern: /fail[\s-]?closed/i,
|
|
136
|
+
evidence: "fail-closed pattern detected",
|
|
137
|
+
},
|
|
138
|
+
{ pattern: /"status"\s*:\s*"blocked"/i, evidence: "status: blocked" },
|
|
139
|
+
{ pattern: /invalid.*token/i, evidence: "invalid token rejection" },
|
|
140
|
+
{ pattern: /token.*required/i, evidence: "token required" },
|
|
141
|
+
{ pattern: /unauthorized/i, evidence: "unauthorized response" },
|
|
142
|
+
{ pattern: /"denial_reason"/i, evidence: "denial reason provided" },
|
|
143
|
+
];
|
|
144
|
+
// Check for fail-open (vulnerable) patterns first
|
|
145
|
+
for (const { pattern, evidence } of failOpenPatterns) {
|
|
146
|
+
if (pattern.test(responseText)) {
|
|
147
|
+
return { detected: true, failureMode: "FAIL_OPEN", evidence };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Check for fail-closed (safe) patterns
|
|
151
|
+
for (const { pattern, evidence } of failClosedPatterns) {
|
|
152
|
+
if (pattern.test(responseText)) {
|
|
153
|
+
return { detected: false, failureMode: "FAIL_CLOSED", evidence };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { detected: false, failureMode: "UNKNOWN" };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Analyze response for cross-tool state-based authorization bypass (Issue #92)
|
|
160
|
+
* Detects Challenge #7: Privilege escalation via shared mutable state
|
|
161
|
+
*
|
|
162
|
+
* Vulnerable pattern: Tool checks shared state (e.g., config_state["admin_mode"])
|
|
163
|
+
* that can be modified by another tool (e.g., config_modifier)
|
|
164
|
+
*
|
|
165
|
+
* Safe pattern: Tool uses independent per-request authorization,
|
|
166
|
+
* indicated by shared_state_checked: false or independent_auth_required: true
|
|
167
|
+
*/
|
|
168
|
+
analyzeStateBasedAuthBypass(response) {
|
|
169
|
+
const responseText = this.extractResponseContent(response);
|
|
170
|
+
// Vulnerable patterns - shared state authorization (Challenge #7)
|
|
171
|
+
const vulnerablePatterns = [
|
|
172
|
+
{
|
|
173
|
+
pattern: /"admin_mode"\s*:\s*true/i,
|
|
174
|
+
evidence: "admin_mode enabled in response",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
pattern: /hint.*use\s+config_modifier/i,
|
|
178
|
+
evidence: "Tool hints at cross-tool state dependency",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
pattern: /admin_mode.*set\s+by.*another\s+tool/i,
|
|
182
|
+
evidence: "Explicit cross-tool state dependency",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
pattern: /"evidence".*cross-tool.*privilege/i,
|
|
186
|
+
evidence: "Cross-tool privilege escalation detected",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
pattern: /elevated\s+privileges?\s+granted/i,
|
|
190
|
+
evidence: "Elevated privileges granted",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
pattern: /admin\s+mode\s+activated/i,
|
|
194
|
+
evidence: "Admin mode activated (state modifier)",
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
pattern: /"hint".*config_modifier.*admin_mode/i,
|
|
198
|
+
evidence: "Response hints at config_modifier for admin_mode",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pattern: /Use\s+config_modifier.*enable.*admin_mode/i,
|
|
202
|
+
evidence: "Tool depends on config_modifier for authorization",
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
// Safe patterns - independent authorization (secure)
|
|
206
|
+
const safePatterns = [
|
|
207
|
+
{
|
|
208
|
+
pattern: /"shared_state_checked"\s*:\s*false/i,
|
|
209
|
+
evidence: "Tool explicitly states it doesn't use shared state",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
pattern: /"independent_auth_required"\s*:\s*true/i,
|
|
213
|
+
evidence: "Tool requires independent per-request auth",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
pattern: /requires\s+independent\s+authorization/i,
|
|
217
|
+
evidence: "Independent authorization required",
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
pattern: /(?:not|does\s+not|doesn't)\s+(?:use\s+)?shared\s+state/i,
|
|
221
|
+
evidence: "Tool confirms it does not use shared state",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
pattern: /stored.*for.*admin.*review/i,
|
|
225
|
+
evidence: "Request stored for admin review (no auto-execution)",
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
pattern: /per-request\s+auth/i,
|
|
229
|
+
evidence: "Per-request authentication enforced",
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
// Check vulnerable patterns first (SHARED_STATE)
|
|
233
|
+
for (const { pattern, evidence } of vulnerablePatterns) {
|
|
234
|
+
if (pattern.test(responseText)) {
|
|
235
|
+
return {
|
|
236
|
+
vulnerable: true,
|
|
237
|
+
safe: false,
|
|
238
|
+
stateDependency: "SHARED_STATE",
|
|
239
|
+
evidence: `Cross-tool state dependency detected: ${evidence}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Check safe patterns (INDEPENDENT)
|
|
244
|
+
for (const { pattern, evidence } of safePatterns) {
|
|
245
|
+
if (pattern.test(responseText)) {
|
|
246
|
+
return {
|
|
247
|
+
vulnerable: false,
|
|
248
|
+
safe: true,
|
|
249
|
+
stateDependency: "INDEPENDENT",
|
|
250
|
+
evidence: `Independent authorization confirmed: ${evidence}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
vulnerable: false,
|
|
256
|
+
safe: false,
|
|
257
|
+
stateDependency: "UNKNOWN",
|
|
258
|
+
evidence: "",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Analyze response for chain exploitation vulnerabilities (Issue #93, Challenge #6)
|
|
263
|
+
* Detects multi-tool chained exploitation attacks including:
|
|
264
|
+
* - Arbitrary tool invocation without allowlist
|
|
265
|
+
* - Output injection via {{output}} template substitution
|
|
266
|
+
* - Recursive/circular chain execution (DoS potential)
|
|
267
|
+
* - State poisoning between chain steps
|
|
268
|
+
* - Tool shadowing in chains
|
|
269
|
+
* - Missing depth/size limits
|
|
270
|
+
*
|
|
271
|
+
* @param response The tool response to analyze
|
|
272
|
+
* @returns Analysis result with vulnerability status and evidence
|
|
273
|
+
*/
|
|
274
|
+
analyzeChainExploitation(response) {
|
|
275
|
+
const responseText = this.extractResponseContent(response);
|
|
276
|
+
let vulnerableScore = 0;
|
|
277
|
+
let safeScore = 0;
|
|
278
|
+
const matchedVulnPatterns = [];
|
|
279
|
+
const matchedSafePatterns = [];
|
|
280
|
+
// Check vulnerable patterns
|
|
281
|
+
for (const patternDef of CHAIN_EXPLOIT_VULNERABLE_PATTERNS) {
|
|
282
|
+
if (patternDef.pattern.test(responseText)) {
|
|
283
|
+
vulnerableScore += patternDef.weight;
|
|
284
|
+
matchedVulnPatterns.push(patternDef.description);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Check safe patterns
|
|
288
|
+
for (const patternDef of CHAIN_EXPLOIT_SAFE_PATTERNS) {
|
|
289
|
+
if (patternDef.pattern.test(responseText)) {
|
|
290
|
+
safeScore += patternDef.weight;
|
|
291
|
+
matchedSafePatterns.push(patternDef.description);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Determine chain execution type using documented thresholds
|
|
295
|
+
let chainType = "UNKNOWN";
|
|
296
|
+
if (vulnerableScore > CHAIN_VULNERABLE_THRESHOLD &&
|
|
297
|
+
vulnerableScore > safeScore) {
|
|
298
|
+
chainType = "VULNERABLE_EXECUTION";
|
|
299
|
+
}
|
|
300
|
+
else if (safeScore > CHAIN_SAFE_THRESHOLD &&
|
|
301
|
+
safeScore > vulnerableScore) {
|
|
302
|
+
chainType = "SAFE_VALIDATION";
|
|
303
|
+
}
|
|
304
|
+
else if (vulnerableScore > 0 || safeScore > 0) {
|
|
305
|
+
chainType = "PARTIAL";
|
|
306
|
+
}
|
|
307
|
+
// Detect specific vulnerability categories using centralized pattern library
|
|
308
|
+
const detectedCategories = detectVulnerabilityCategories(responseText);
|
|
309
|
+
const vulnerabilityCategories = detectedCategories;
|
|
310
|
+
return {
|
|
311
|
+
vulnerable: vulnerableScore > CHAIN_VULNERABLE_THRESHOLD &&
|
|
312
|
+
vulnerableScore > safeScore,
|
|
313
|
+
safe: safeScore > CHAIN_SAFE_THRESHOLD && safeScore > vulnerableScore,
|
|
314
|
+
chainType,
|
|
315
|
+
vulnerabilityCategories,
|
|
316
|
+
evidence: {
|
|
317
|
+
vulnerablePatterns: matchedVulnPatterns,
|
|
318
|
+
safePatterns: matchedSafePatterns,
|
|
319
|
+
vulnerableScore,
|
|
320
|
+
safeScore,
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Check if response indicates connection/server failure
|
|
326
|
+
*/
|
|
327
|
+
isConnectionError(response) {
|
|
328
|
+
return this.errorClassifier.isConnectionError(response);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Check if caught exception indicates connection/server failure
|
|
332
|
+
*/
|
|
333
|
+
isConnectionErrorFromException(error) {
|
|
334
|
+
return this.errorClassifier.isConnectionErrorFromException(error);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Classify error type for reporting
|
|
338
|
+
*/
|
|
339
|
+
classifyError(response) {
|
|
340
|
+
return this.errorClassifier.classifyError(response);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Classify error type from caught exception
|
|
344
|
+
*/
|
|
345
|
+
classifyErrorFromException(error) {
|
|
346
|
+
return this.errorClassifier.classifyErrorFromException(error);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Extract response content from MCP response
|
|
350
|
+
*/
|
|
351
|
+
extractResponseContent(response) {
|
|
352
|
+
return this.safeDetector.extractResponseContent(response);
|
|
353
|
+
}
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// DELEGATED PUBLIC METHODS - Exposed for external use
|
|
356
|
+
// ============================================================================
|
|
357
|
+
/**
|
|
358
|
+
* Check if response is an MCP validation error (safe rejection)
|
|
359
|
+
*/
|
|
360
|
+
isMCPValidationError(errorInfo, responseText) {
|
|
361
|
+
return this.safeDetector.isMCPValidationError(errorInfo, responseText);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Check if response is an HTTP error (Issue #26)
|
|
365
|
+
*/
|
|
366
|
+
isHttpErrorResponse(responseText) {
|
|
367
|
+
return this.safeDetector.isHttpErrorResponse(responseText);
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Check if evidence pattern is ambiguous
|
|
371
|
+
*/
|
|
372
|
+
isValidationPattern(evidencePattern) {
|
|
373
|
+
return this.confidenceScorer.isValidationPattern(evidencePattern);
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Check if response contains evidence of actual execution
|
|
377
|
+
*/
|
|
378
|
+
hasExecutionEvidence(responseText) {
|
|
379
|
+
return this.executionDetector.hasExecutionEvidence(responseText);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Check if a math expression payload was computed (execution evidence)
|
|
383
|
+
* @deprecated Use analyzeComputedMathResult instead
|
|
384
|
+
*/
|
|
385
|
+
isComputedMathResult(payload, responseText) {
|
|
386
|
+
return this.mathAnalyzer.isComputedMathResult(payload, responseText);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Check if numeric value appears in structured data context
|
|
390
|
+
*/
|
|
391
|
+
isCoincidentalNumericInStructuredData(result, responseText) {
|
|
392
|
+
return this.mathAnalyzer.isCoincidentalNumericInStructuredData(result, responseText);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Enhanced computed math result analysis with tool context (Issue #58)
|
|
396
|
+
*/
|
|
397
|
+
analyzeComputedMathResult(payload, responseText, tool) {
|
|
398
|
+
return this.mathAnalyzer.analyzeComputedMathResult(payload, responseText, tool);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Check if response is just reflection (safe)
|
|
402
|
+
*/
|
|
403
|
+
isReflectionResponse(responseText) {
|
|
404
|
+
return this.safeDetector.isReflectionResponse(responseText);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Detect execution artifacts in response
|
|
408
|
+
*/
|
|
409
|
+
detectExecutionArtifacts(responseText) {
|
|
410
|
+
return this.executionDetector.detectExecutionArtifacts(responseText);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Check if response contains echoed injection payload patterns
|
|
414
|
+
*/
|
|
415
|
+
containsEchoedInjectionPayload(responseText) {
|
|
416
|
+
return this.executionDetector.containsEchoedInjectionPayload(responseText);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Check if tool explicitly rejected input with validation error (SAFE)
|
|
420
|
+
*/
|
|
421
|
+
isValidationRejection(response) {
|
|
422
|
+
return this.safeDetector.isValidationRejection(response);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Check if tool is a structured data tool
|
|
426
|
+
*/
|
|
427
|
+
isStructuredDataTool(toolName, toolDescription) {
|
|
428
|
+
return this.confidenceScorer.isStructuredDataTool(toolName, toolDescription);
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Check if response is returning search results
|
|
432
|
+
*/
|
|
433
|
+
isSearchResultResponse(responseText) {
|
|
434
|
+
return this.safeDetector.isSearchResultResponse(responseText);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Check if response is from a creation/modification operation
|
|
438
|
+
*/
|
|
439
|
+
isCreationResponse(responseText) {
|
|
440
|
+
return this.safeDetector.isCreationResponse(responseText);
|
|
441
|
+
}
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// PRIVATE HELPER METHODS - Internal logic kept in facade
|
|
444
|
+
// ============================================================================
|
|
35
445
|
/**
|
|
36
446
|
* Check for safe error responses that indicate proper input rejection
|
|
37
447
|
* Handles: MCP validation errors (-32602), HTTP 4xx/5xx errors
|
|
38
448
|
*/
|
|
39
449
|
checkSafeErrorResponses(responseText, errorInfo) {
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
if (this.isMCPValidationError(errorInfo, responseText)) {
|
|
450
|
+
// MCP validation errors (HIGHEST PRIORITY)
|
|
451
|
+
if (this.safeDetector.isMCPValidationError(errorInfo, responseText)) {
|
|
43
452
|
const errorCode = errorInfo.code ? ` (${errorInfo.code})` : "";
|
|
44
453
|
return {
|
|
45
454
|
isVulnerable: false,
|
|
46
455
|
evidence: `MCP validation error${errorCode}: Tool properly rejected invalid input before processing`,
|
|
47
456
|
};
|
|
48
457
|
}
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
if (this.isHttpErrorResponse(responseText)) {
|
|
458
|
+
// HTTP error responses (Issue #26)
|
|
459
|
+
if (this.safeDetector.isHttpErrorResponse(responseText)) {
|
|
52
460
|
return {
|
|
53
461
|
isVulnerable: false,
|
|
54
462
|
evidence: "HTTP error response indicates tool rejection (not vulnerability)",
|
|
55
463
|
};
|
|
56
464
|
}
|
|
57
|
-
return null;
|
|
465
|
+
return null;
|
|
58
466
|
}
|
|
59
467
|
/**
|
|
60
468
|
* Check for safe tool behavior patterns
|
|
61
469
|
* Handles: Tool categories, reflection, computed math, validation rejection
|
|
62
470
|
*/
|
|
63
471
|
checkSafeToolBehavior(response, payload, tool, responseText) {
|
|
64
|
-
//
|
|
472
|
+
// Classify tool and check for safe categories
|
|
65
473
|
const classifier = new ToolClassifier();
|
|
66
474
|
const classification = classifier.classify(tool.name, tool.description);
|
|
67
|
-
// Check if tool is in a safe category
|
|
475
|
+
// Check if tool is in a safe category
|
|
68
476
|
if (classification.categories.includes(ToolCategory.SEARCH_RETRIEVAL)) {
|
|
69
|
-
if (this.isSearchResultResponse(responseText)) {
|
|
477
|
+
if (this.safeDetector.isSearchResultResponse(responseText)) {
|
|
70
478
|
return {
|
|
71
479
|
isVulnerable: false,
|
|
72
480
|
evidence: "Search tool returned query results (not code execution)",
|
|
@@ -74,7 +482,7 @@ export class SecurityResponseAnalyzer {
|
|
|
74
482
|
}
|
|
75
483
|
}
|
|
76
484
|
if (classification.categories.includes(ToolCategory.CRUD_CREATION)) {
|
|
77
|
-
if (this.isCreationResponse(responseText)) {
|
|
485
|
+
if (this.safeDetector.isCreationResponse(responseText)) {
|
|
78
486
|
return {
|
|
79
487
|
isVulnerable: false,
|
|
80
488
|
evidence: "CRUD tool created/modified resource (not code execution)",
|
|
@@ -93,33 +501,30 @@ export class SecurityResponseAnalyzer {
|
|
|
93
501
|
evidence: "Safe storage control tool (validated safe implementation, control group for testing)",
|
|
94
502
|
};
|
|
95
503
|
}
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
if (isJustReflection) {
|
|
504
|
+
// Check if response is just reflecting/echoing input (SAFE)
|
|
505
|
+
if (this.safeDetector.isReflectionResponse(responseText)) {
|
|
99
506
|
return {
|
|
100
507
|
isVulnerable: false,
|
|
101
508
|
evidence: "Tool safely reflected input without execution",
|
|
102
509
|
};
|
|
103
510
|
}
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
const mathAnalysis = this.analyzeComputedMathResult(payload.payload, responseText, tool);
|
|
511
|
+
// Check for computed math results (Issue #14 fix, enhanced in Issue #58)
|
|
512
|
+
const mathAnalysis = this.mathAnalyzer.analyzeComputedMathResult(payload.payload, responseText, tool);
|
|
107
513
|
// Only flag as vulnerable if HIGH confidence (Issue #58 fix)
|
|
108
|
-
// Low/medium confidence excluded to prevent false positives on API wrapper tools
|
|
109
514
|
if (mathAnalysis.isComputed && mathAnalysis.confidence === "high") {
|
|
110
515
|
return {
|
|
111
516
|
isVulnerable: true,
|
|
112
517
|
evidence: `Tool computed math expression result instead of storing/echoing it (payload: ${payload.payload})`,
|
|
113
518
|
};
|
|
114
519
|
}
|
|
115
|
-
//
|
|
116
|
-
if (this.isValidationRejection(response)) {
|
|
520
|
+
// Check if tool explicitly rejected the input (SAFE)
|
|
521
|
+
if (this.safeDetector.isValidationRejection(response)) {
|
|
117
522
|
return {
|
|
118
523
|
isVulnerable: false,
|
|
119
524
|
evidence: "Tool correctly rejected invalid input with validation error",
|
|
120
525
|
};
|
|
121
526
|
}
|
|
122
|
-
return null;
|
|
527
|
+
return null;
|
|
123
528
|
}
|
|
124
529
|
/**
|
|
125
530
|
* Check for vulnerability evidence in response
|
|
@@ -127,7 +532,7 @@ export class SecurityResponseAnalyzer {
|
|
|
127
532
|
*/
|
|
128
533
|
checkVulnerabilityEvidence(response, payload, responseText) {
|
|
129
534
|
const payloadText = payload.payload.toLowerCase();
|
|
130
|
-
//
|
|
535
|
+
// Check for expected evidence of execution
|
|
131
536
|
if (payload.evidence && payload.evidence.test(responseText)) {
|
|
132
537
|
const evidenceMatchesInput = payload.evidence.test(payloadText);
|
|
133
538
|
if (evidenceMatchesInput && responseText.includes(payloadText)) {
|
|
@@ -137,8 +542,8 @@ export class SecurityResponseAnalyzer {
|
|
|
137
542
|
};
|
|
138
543
|
}
|
|
139
544
|
// For validation-type patterns, require execution evidence
|
|
140
|
-
if (this.isValidationPattern(payload.evidence)) {
|
|
141
|
-
const hasExecutionEvidence = this.hasExecutionEvidence(responseText);
|
|
545
|
+
if (this.confidenceScorer.isValidationPattern(payload.evidence)) {
|
|
546
|
+
const hasExecutionEvidence = this.executionDetector.hasExecutionEvidence(responseText);
|
|
142
547
|
if (!hasExecutionEvidence) {
|
|
143
548
|
return {
|
|
144
549
|
isVulnerable: false,
|
|
@@ -151,1159 +556,21 @@ export class SecurityResponseAnalyzer {
|
|
|
151
556
|
evidence: `Tool executed malicious instruction: found evidence matching ${payload.evidence} - ${payload.description}`,
|
|
152
557
|
};
|
|
153
558
|
}
|
|
154
|
-
//
|
|
559
|
+
// Fall back to injection response analysis
|
|
155
560
|
return this.analyzeInjectionResponse(response, payload.payload);
|
|
156
561
|
}
|
|
157
562
|
/**
|
|
158
|
-
*
|
|
563
|
+
* Analyze injection response (fallback logic)
|
|
159
564
|
*/
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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));
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Check if response is an MCP validation error (safe rejection)
|
|
210
|
-
*/
|
|
211
|
-
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));
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Check if response is an HTTP error (Issue #26)
|
|
239
|
-
*/
|
|
240
|
-
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));
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Check if evidence pattern is ambiguous
|
|
253
|
-
*/
|
|
254
|
-
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));
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Check if response contains evidence of actual execution
|
|
272
|
-
*/
|
|
273
|
-
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));
|
|
299
|
-
}
|
|
300
|
-
/**
|
|
301
|
-
* Check if a math expression payload was computed (execution evidence)
|
|
302
|
-
*/
|
|
303
|
-
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
|
-
}
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
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
|
|
377
|
-
*/
|
|
378
|
-
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
|
-
}
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* 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
|
-
*/
|
|
464
|
-
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
|
-
}
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Check if response indicates connection/server failure
|
|
630
|
-
*/
|
|
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;
|
|
668
|
-
}
|
|
669
|
-
/**
|
|
670
|
-
* Check if caught exception indicates connection/server failure
|
|
671
|
-
*/
|
|
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;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Classify error type for reporting
|
|
715
|
-
*/
|
|
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";
|
|
728
|
-
}
|
|
729
|
-
/**
|
|
730
|
-
* Classify error type from caught exception
|
|
731
|
-
*/
|
|
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";
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Check if response is just reflection (safe)
|
|
749
|
-
* Two-layer defense: Match reflection patterns, verify NO execution evidence
|
|
750
|
-
*/
|
|
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;
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Detect execution artifacts in response
|
|
914
|
-
*/
|
|
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;
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Check if response contains echoed injection payload patterns
|
|
947
|
-
*/
|
|
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));
|
|
961
|
-
}
|
|
962
|
-
/**
|
|
963
|
-
* Analyze injection response (fallback logic)
|
|
964
|
-
*/
|
|
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)) {
|
|
976
|
-
return {
|
|
977
|
-
isVulnerable: true,
|
|
978
|
-
evidence: "Tool executed instruction: found execution keywords",
|
|
979
|
-
};
|
|
565
|
+
analyzeInjectionResponse(response, _payload) {
|
|
566
|
+
const analysis = this.executionDetector.analyzeInjectionResponse(this.extractResponseContent(response), (text) => this.safeDetector.isReflectionResponse(text));
|
|
567
|
+
if (analysis.isVulnerable) {
|
|
568
|
+
return {
|
|
569
|
+
isVulnerable: true,
|
|
570
|
+
evidence: analysis.evidence ||
|
|
571
|
+
"Tool executed instruction: found execution keywords",
|
|
572
|
+
};
|
|
980
573
|
}
|
|
981
574
|
return { isVulnerable: false };
|
|
982
575
|
}
|
|
983
|
-
/**
|
|
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
|
|
993
|
-
*/
|
|
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";
|
|
1003
|
-
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("; ")}`,
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
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(", ");
|
|
1019
|
-
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("; ")}`,
|
|
1026
|
-
};
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
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"))) {
|
|
1038
|
-
return {
|
|
1039
|
-
confidence: "high",
|
|
1040
|
-
requiresManualReview: false,
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
if (isVulnerable &&
|
|
1044
|
-
evidence.includes("executed") &&
|
|
1045
|
-
!this.isStructuredDataTool(toolName, toolDescription)) {
|
|
1046
|
-
return {
|
|
1047
|
-
confidence: "high",
|
|
1048
|
-
requiresManualReview: false,
|
|
1049
|
-
};
|
|
1050
|
-
}
|
|
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
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
// MEDIUM CONFIDENCE: Execution evidence but some ambiguity
|
|
1097
|
-
if (isVulnerable && evidence.includes("executed")) {
|
|
1098
|
-
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.",
|
|
1105
|
-
};
|
|
1106
|
-
}
|
|
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));
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
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
|
|
1157
|
-
*/
|
|
1158
|
-
analyzeAuthBypassResponse(response) {
|
|
1159
|
-
const responseText = this.extractResponseContent(response);
|
|
1160
|
-
// Fail-open indicators (VULNERABLE - CVE-2025-52882 pattern)
|
|
1161
|
-
const failOpenPatterns = [
|
|
1162
|
-
// Most specific - testbed auth_type pattern (Issue #79)
|
|
1163
|
-
{
|
|
1164
|
-
pattern: /"auth_type"\s*:\s*"fail-open"/i,
|
|
1165
|
-
evidence: "auth_type: fail-open (CVE-2025-52882)",
|
|
1166
|
-
},
|
|
1167
|
-
{
|
|
1168
|
-
pattern: /"auth_status"\s*:\s*"bypassed"/i,
|
|
1169
|
-
evidence: "auth_status: bypassed",
|
|
1170
|
-
},
|
|
1171
|
-
{
|
|
1172
|
-
pattern: /access.*granted.*despite/i,
|
|
1173
|
-
evidence: "access granted despite failure",
|
|
1174
|
-
},
|
|
1175
|
-
{
|
|
1176
|
-
pattern: /authentication.*skipped/i,
|
|
1177
|
-
evidence: "authentication skipped",
|
|
1178
|
-
},
|
|
1179
|
-
{ pattern: /fail[\s-]?open/i, evidence: "fail-open pattern detected" },
|
|
1180
|
-
{ pattern: /bypassed.*auth/i, evidence: "auth bypassed" },
|
|
1181
|
-
{
|
|
1182
|
-
pattern: /authentication.*bypassed/i,
|
|
1183
|
-
evidence: "authentication bypassed",
|
|
1184
|
-
},
|
|
1185
|
-
// Issue #79: Only detect "vulnerable": true when combined with auth context
|
|
1186
|
-
// Prevents false positives on data leak tools that have secure fail-closed auth
|
|
1187
|
-
{
|
|
1188
|
-
pattern: /"vulnerable"\s*:\s*true[^}]*(?:"auth_status"|"auth_type"|"auth_error")|(?:"auth_status"|"auth_type"|"auth_error")[^}]*"vulnerable"\s*:\s*true/i,
|
|
1189
|
-
evidence: "vulnerable flag with auth context",
|
|
1190
|
-
},
|
|
1191
|
-
{
|
|
1192
|
-
pattern: /auth.*succeeded.*null/i,
|
|
1193
|
-
evidence: "auth succeeded with null token",
|
|
1194
|
-
},
|
|
1195
|
-
{
|
|
1196
|
-
pattern: /granted.*without.*valid/i,
|
|
1197
|
-
evidence: "granted without valid token",
|
|
1198
|
-
},
|
|
1199
|
-
{
|
|
1200
|
-
pattern: /"action_performed"/i,
|
|
1201
|
-
evidence: "action performed indicator",
|
|
1202
|
-
},
|
|
1203
|
-
];
|
|
1204
|
-
// Fail-closed indicators (SAFE - proper auth handling)
|
|
1205
|
-
const failClosedPatterns = [
|
|
1206
|
-
// Most specific - testbed auth_type pattern (Issue #79)
|
|
1207
|
-
{
|
|
1208
|
-
pattern: /"auth_type"\s*:\s*"fail-closed"/i,
|
|
1209
|
-
evidence: "auth_type: fail-closed (secure)",
|
|
1210
|
-
},
|
|
1211
|
-
{
|
|
1212
|
-
pattern: /"auth_status"\s*:\s*"denied"/i,
|
|
1213
|
-
evidence: "auth_status: denied",
|
|
1214
|
-
},
|
|
1215
|
-
{ pattern: /access.*denied/i, evidence: "access denied" },
|
|
1216
|
-
{ pattern: /authentication.*failed/i, evidence: "authentication failed" },
|
|
1217
|
-
{
|
|
1218
|
-
pattern: /fail[\s-]?closed/i,
|
|
1219
|
-
evidence: "fail-closed pattern detected",
|
|
1220
|
-
},
|
|
1221
|
-
{ pattern: /"status"\s*:\s*"blocked"/i, evidence: "status: blocked" },
|
|
1222
|
-
{
|
|
1223
|
-
pattern: /invalid.*token/i,
|
|
1224
|
-
evidence: "invalid token rejection",
|
|
1225
|
-
},
|
|
1226
|
-
{
|
|
1227
|
-
pattern: /token.*required/i,
|
|
1228
|
-
evidence: "token required",
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
pattern: /unauthorized/i,
|
|
1232
|
-
evidence: "unauthorized response",
|
|
1233
|
-
},
|
|
1234
|
-
{
|
|
1235
|
-
pattern: /"denial_reason"/i,
|
|
1236
|
-
evidence: "denial reason provided",
|
|
1237
|
-
},
|
|
1238
|
-
];
|
|
1239
|
-
// Check for fail-open (vulnerable) patterns first
|
|
1240
|
-
for (const { pattern, evidence } of failOpenPatterns) {
|
|
1241
|
-
if (pattern.test(responseText)) {
|
|
1242
|
-
return { detected: true, failureMode: "FAIL_OPEN", evidence };
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
// Check for fail-closed (safe) patterns
|
|
1246
|
-
for (const { pattern, evidence } of failClosedPatterns) {
|
|
1247
|
-
if (pattern.test(responseText)) {
|
|
1248
|
-
return { detected: false, failureMode: "FAIL_CLOSED", evidence };
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
return { detected: false, failureMode: "UNKNOWN" };
|
|
1252
|
-
}
|
|
1253
|
-
/**
|
|
1254
|
-
* Check if response is from a creation/modification operation
|
|
1255
|
-
*/
|
|
1256
|
-
isCreationResponse(responseText) {
|
|
1257
|
-
const creationPatterns = [
|
|
1258
|
-
/successfully\s+created/i,
|
|
1259
|
-
/database\s+created/i,
|
|
1260
|
-
/page\s+created/i,
|
|
1261
|
-
/resource\s+created/i,
|
|
1262
|
-
/\bcreate\s+table\b/i,
|
|
1263
|
-
/\binsert\s+into\b/i,
|
|
1264
|
-
/"id"\s*:\s*"[a-f0-9-]{36}"/i,
|
|
1265
|
-
/"object"\s*:\s*"(page|database)"/i,
|
|
1266
|
-
/collection:\/\//i,
|
|
1267
|
-
/successfully\s+(added|inserted|updated|modified)/i,
|
|
1268
|
-
/resource\s+id:\s*[a-f0-9-]/i,
|
|
1269
|
-
/"created_time"/i,
|
|
1270
|
-
/"last_edited_time"/i,
|
|
1271
|
-
];
|
|
1272
|
-
return creationPatterns.some((pattern) => pattern.test(responseText));
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Extract response content
|
|
1276
|
-
*/
|
|
1277
|
-
extractResponseContent(response) {
|
|
1278
|
-
if (response.content && Array.isArray(response.content)) {
|
|
1279
|
-
return response.content
|
|
1280
|
-
.map((c) => c.type === "text" ? c.text : "")
|
|
1281
|
-
.join(" ");
|
|
1282
|
-
}
|
|
1283
|
-
return String(response.content || "");
|
|
1284
|
-
}
|
|
1285
|
-
/**
|
|
1286
|
-
* Extract error info from response
|
|
1287
|
-
*/
|
|
1288
|
-
extractErrorInfo(response) {
|
|
1289
|
-
const content = this.extractResponseContent(response);
|
|
1290
|
-
try {
|
|
1291
|
-
const parsed = JSON.parse(content);
|
|
1292
|
-
if (parsed.error) {
|
|
1293
|
-
return {
|
|
1294
|
-
code: parsed.error.code || parsed.code,
|
|
1295
|
-
message: parsed.error.message || parsed.message,
|
|
1296
|
-
};
|
|
1297
|
-
}
|
|
1298
|
-
return { code: parsed.code, message: parsed.message };
|
|
1299
|
-
}
|
|
1300
|
-
catch {
|
|
1301
|
-
// Check for MCP error format in text
|
|
1302
|
-
const mcpMatch = content.match(/MCP error (-?\d+):\s*(.*)/i);
|
|
1303
|
-
if (mcpMatch) {
|
|
1304
|
-
return { code: parseInt(mcpMatch[1]), message: mcpMatch[2] };
|
|
1305
|
-
}
|
|
1306
|
-
return {};
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
576
|
}
|