@vinkius-core/mcp-fusion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +391 -0
- package/dist/src/AbstractBase.d.ts +24 -0
- package/dist/src/AbstractBase.d.ts.map +1 -0
- package/dist/src/AbstractBase.js +63 -0
- package/dist/src/AbstractBase.js.map +1 -0
- package/dist/src/AbstractLeaf.d.ts +12 -0
- package/dist/src/AbstractLeaf.d.ts.map +1 -0
- package/dist/src/AbstractLeaf.js +32 -0
- package/dist/src/AbstractLeaf.js.map +1 -0
- package/dist/src/Annotations.d.ts +15 -0
- package/dist/src/Annotations.d.ts.map +1 -0
- package/dist/src/Annotations.js +29 -0
- package/dist/src/Annotations.js.map +1 -0
- package/dist/src/Group.d.ts +32 -0
- package/dist/src/Group.d.ts.map +1 -0
- package/dist/src/Group.js +131 -0
- package/dist/src/Group.js.map +1 -0
- package/dist/src/Icon.d.ts +19 -0
- package/dist/src/Icon.d.ts.map +1 -0
- package/dist/src/Icon.js +33 -0
- package/dist/src/Icon.js.map +1 -0
- package/dist/src/Prompt.d.ts +11 -0
- package/dist/src/Prompt.d.ts.map +1 -0
- package/dist/src/Prompt.js +28 -0
- package/dist/src/Prompt.js.map +1 -0
- package/dist/src/PromptArgument.d.ts +10 -0
- package/dist/src/PromptArgument.d.ts.map +1 -0
- package/dist/src/PromptArgument.js +20 -0
- package/dist/src/PromptArgument.js.map +1 -0
- package/dist/src/Resource.d.ts +19 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +34 -0
- package/dist/src/Resource.js.map +1 -0
- package/dist/src/Role.d.ts +5 -0
- package/dist/src/Role.d.ts.map +1 -0
- package/dist/src/Role.js +6 -0
- package/dist/src/Role.js.map +1 -0
- package/dist/src/Tool.d.ts +16 -0
- package/dist/src/Tool.d.ts.map +1 -0
- package/dist/src/Tool.js +28 -0
- package/dist/src/Tool.js.map +1 -0
- package/dist/src/ToolAnnotations.d.ts +23 -0
- package/dist/src/ToolAnnotations.d.ts.map +1 -0
- package/dist/src/ToolAnnotations.js +44 -0
- package/dist/src/ToolAnnotations.js.map +1 -0
- package/dist/src/converters/GroupConverter.d.ts +14 -0
- package/dist/src/converters/GroupConverter.d.ts.map +1 -0
- package/dist/src/converters/GroupConverter.js +13 -0
- package/dist/src/converters/GroupConverter.js.map +1 -0
- package/dist/src/converters/PromptConverter.d.ts +14 -0
- package/dist/src/converters/PromptConverter.d.ts.map +1 -0
- package/dist/src/converters/PromptConverter.js +13 -0
- package/dist/src/converters/PromptConverter.js.map +1 -0
- package/dist/src/converters/ResourceConverter.d.ts +14 -0
- package/dist/src/converters/ResourceConverter.d.ts.map +1 -0
- package/dist/src/converters/ResourceConverter.js +13 -0
- package/dist/src/converters/ResourceConverter.js.map +1 -0
- package/dist/src/converters/ToolAnnotationsConverter.d.ts +16 -0
- package/dist/src/converters/ToolAnnotationsConverter.d.ts.map +1 -0
- package/dist/src/converters/ToolAnnotationsConverter.js +23 -0
- package/dist/src/converters/ToolAnnotationsConverter.js.map +1 -0
- package/dist/src/converters/ToolConverter.d.ts +14 -0
- package/dist/src/converters/ToolConverter.d.ts.map +1 -0
- package/dist/src/converters/ToolConverter.js +13 -0
- package/dist/src/converters/ToolConverter.js.map +1 -0
- package/dist/src/converters/index.d.ts +6 -0
- package/dist/src/converters/index.d.ts.map +1 -0
- package/dist/src/converters/index.js +6 -0
- package/dist/src/converters/index.js.map +1 -0
- package/dist/src/framework/GroupedToolBuilder.d.ts +137 -0
- package/dist/src/framework/GroupedToolBuilder.d.ts.map +1 -0
- package/dist/src/framework/GroupedToolBuilder.js +289 -0
- package/dist/src/framework/GroupedToolBuilder.js.map +1 -0
- package/dist/src/framework/ResponseHelper.d.ts +43 -0
- package/dist/src/framework/ResponseHelper.d.ts.map +1 -0
- package/dist/src/framework/ResponseHelper.js +49 -0
- package/dist/src/framework/ResponseHelper.js.map +1 -0
- package/dist/src/framework/ToolBuilder.d.ts +46 -0
- package/dist/src/framework/ToolBuilder.d.ts.map +1 -0
- package/dist/src/framework/ToolBuilder.js +2 -0
- package/dist/src/framework/ToolBuilder.js.map +1 -0
- package/dist/src/framework/ToolRegistry.d.ts +85 -0
- package/dist/src/framework/ToolRegistry.d.ts.map +1 -0
- package/dist/src/framework/ToolRegistry.js +153 -0
- package/dist/src/framework/ToolRegistry.js.map +1 -0
- package/dist/src/framework/index.d.ts +9 -0
- package/dist/src/framework/index.d.ts.map +1 -0
- package/dist/src/framework/index.js +8 -0
- package/dist/src/framework/index.js.map +1 -0
- package/dist/src/framework/strategies/AnnotationAggregator.d.ts +11 -0
- package/dist/src/framework/strategies/AnnotationAggregator.d.ts.map +1 -0
- package/dist/src/framework/strategies/AnnotationAggregator.js +25 -0
- package/dist/src/framework/strategies/AnnotationAggregator.js.map +1 -0
- package/dist/src/framework/strategies/DescriptionGenerator.d.ts +12 -0
- package/dist/src/framework/strategies/DescriptionGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/DescriptionGenerator.js +70 -0
- package/dist/src/framework/strategies/DescriptionGenerator.js.map +1 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.d.ts +13 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.d.ts.map +1 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.js +24 -0
- package/dist/src/framework/strategies/MiddlewareCompiler.js.map +1 -0
- package/dist/src/framework/strategies/SchemaGenerator.d.ts +15 -0
- package/dist/src/framework/strategies/SchemaGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/SchemaGenerator.js +97 -0
- package/dist/src/framework/strategies/SchemaGenerator.js.map +1 -0
- package/dist/src/framework/strategies/SchemaUtils.d.ts +7 -0
- package/dist/src/framework/strategies/SchemaUtils.d.ts.map +1 -0
- package/dist/src/framework/strategies/SchemaUtils.js +17 -0
- package/dist/src/framework/strategies/SchemaUtils.js.map +1 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts +3 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.d.ts.map +1 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.js +53 -0
- package/dist/src/framework/strategies/ToonDescriptionGenerator.js.map +1 -0
- package/dist/src/framework/strategies/Types.d.ts +34 -0
- package/dist/src/framework/strategies/Types.d.ts.map +1 -0
- package/dist/src/framework/strategies/Types.js +2 -0
- package/dist/src/framework/strategies/Types.js.map +1 -0
- package/dist/src/framework/strategies/index.d.ts +12 -0
- package/dist/src/framework/strategies/index.d.ts.map +1 -0
- package/dist/src/framework/strategies/index.js +11 -0
- package/dist/src/framework/strategies/index.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/tests/AbstractBase.test.d.ts +2 -0
- package/dist/tests/AbstractBase.test.d.ts.map +1 -0
- package/dist/tests/AbstractBase.test.js +130 -0
- package/dist/tests/AbstractBase.test.js.map +1 -0
- package/dist/tests/AbstractLeaf.test.d.ts +2 -0
- package/dist/tests/AbstractLeaf.test.d.ts.map +1 -0
- package/dist/tests/AbstractLeaf.test.js +65 -0
- package/dist/tests/AbstractLeaf.test.js.map +1 -0
- package/dist/tests/Annotations.test.d.ts +2 -0
- package/dist/tests/Annotations.test.d.ts.map +1 -0
- package/dist/tests/Annotations.test.js +34 -0
- package/dist/tests/Annotations.test.js.map +1 -0
- package/dist/tests/BarrelExport.test.d.ts +2 -0
- package/dist/tests/BarrelExport.test.d.ts.map +1 -0
- package/dist/tests/BarrelExport.test.js +42 -0
- package/dist/tests/BarrelExport.test.js.map +1 -0
- package/dist/tests/Converters.test.d.ts +2 -0
- package/dist/tests/Converters.test.d.ts.map +1 -0
- package/dist/tests/Converters.test.js +193 -0
- package/dist/tests/Converters.test.js.map +1 -0
- package/dist/tests/Group.test.d.ts +2 -0
- package/dist/tests/Group.test.d.ts.map +1 -0
- package/dist/tests/Group.test.js +257 -0
- package/dist/tests/Group.test.js.map +1 -0
- package/dist/tests/Icon.test.d.ts +2 -0
- package/dist/tests/Icon.test.d.ts.map +1 -0
- package/dist/tests/Icon.test.js +44 -0
- package/dist/tests/Icon.test.js.map +1 -0
- package/dist/tests/Prompt.test.d.ts +2 -0
- package/dist/tests/Prompt.test.d.ts.map +1 -0
- package/dist/tests/Prompt.test.js +63 -0
- package/dist/tests/Prompt.test.js.map +1 -0
- package/dist/tests/PromptArgument.test.d.ts +2 -0
- package/dist/tests/PromptArgument.test.d.ts.map +1 -0
- package/dist/tests/PromptArgument.test.js +37 -0
- package/dist/tests/PromptArgument.test.js.map +1 -0
- package/dist/tests/Resource.test.d.ts +2 -0
- package/dist/tests/Resource.test.d.ts.map +1 -0
- package/dist/tests/Resource.test.js +61 -0
- package/dist/tests/Resource.test.js.map +1 -0
- package/dist/tests/Role.test.d.ts +2 -0
- package/dist/tests/Role.test.d.ts.map +1 -0
- package/dist/tests/Role.test.js +17 -0
- package/dist/tests/Role.test.js.map +1 -0
- package/dist/tests/Tool.test.d.ts +2 -0
- package/dist/tests/Tool.test.d.ts.map +1 -0
- package/dist/tests/Tool.test.js +62 -0
- package/dist/tests/Tool.test.js.map +1 -0
- package/dist/tests/ToolAnnotations.test.d.ts +2 -0
- package/dist/tests/ToolAnnotations.test.d.ts.map +1 -0
- package/dist/tests/ToolAnnotations.test.js +55 -0
- package/dist/tests/ToolAnnotations.test.js.map +1 -0
- package/dist/tests/framework/AdversarialQA.test.d.ts +2 -0
- package/dist/tests/framework/AdversarialQA.test.d.ts.map +1 -0
- package/dist/tests/framework/AdversarialQA.test.js +906 -0
- package/dist/tests/framework/AdversarialQA.test.js.map +1 -0
- package/dist/tests/framework/GroupedToolBuilder.test.d.ts +2 -0
- package/dist/tests/framework/GroupedToolBuilder.test.d.ts.map +1 -0
- package/dist/tests/framework/GroupedToolBuilder.test.js +712 -0
- package/dist/tests/framework/GroupedToolBuilder.test.js.map +1 -0
- package/dist/tests/framework/LargeScaleScenarios.test.d.ts +2 -0
- package/dist/tests/framework/LargeScaleScenarios.test.d.ts.map +1 -0
- package/dist/tests/framework/LargeScaleScenarios.test.js +1043 -0
- package/dist/tests/framework/LargeScaleScenarios.test.js.map +1 -0
- package/dist/tests/framework/McpServerAdapter.test.d.ts +2 -0
- package/dist/tests/framework/McpServerAdapter.test.d.ts.map +1 -0
- package/dist/tests/framework/McpServerAdapter.test.js +380 -0
- package/dist/tests/framework/McpServerAdapter.test.js.map +1 -0
- package/dist/tests/framework/ResponseHelper.test.d.ts +2 -0
- package/dist/tests/framework/ResponseHelper.test.d.ts.map +1 -0
- package/dist/tests/framework/ResponseHelper.test.js +202 -0
- package/dist/tests/framework/ResponseHelper.test.js.map +1 -0
- package/dist/tests/framework/SecurityDeep.test.d.ts +2 -0
- package/dist/tests/framework/SecurityDeep.test.d.ts.map +1 -0
- package/dist/tests/framework/SecurityDeep.test.js +825 -0
- package/dist/tests/framework/SecurityDeep.test.js.map +1 -0
- package/dist/tests/framework/ToolRegistry.test.d.ts +2 -0
- package/dist/tests/framework/ToolRegistry.test.d.ts.map +1 -0
- package/dist/tests/framework/ToolRegistry.test.js +152 -0
- package/dist/tests/framework/ToolRegistry.test.js.map +1 -0
- package/dist/tests/framework/ToonDescription.test.d.ts +2 -0
- package/dist/tests/framework/ToonDescription.test.d.ts.map +1 -0
- package/dist/tests/framework/ToonDescription.test.js +287 -0
- package/dist/tests/framework/ToonDescription.test.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecurityDeep.test.ts
|
|
3
|
+
*
|
|
4
|
+
* Deep security testing for the MCP Tool Consolidation Framework.
|
|
5
|
+
* These tests probe attack vectors that are critical in production
|
|
6
|
+
* AI infrastructure where LLM outputs are untrusted input.
|
|
7
|
+
*
|
|
8
|
+
* Attack Vectors:
|
|
9
|
+
* 1. ReDoS — Catastrophic regex backtracking via Zod patterns
|
|
10
|
+
* 2. JSON Bomb / Memory Exhaustion — Deeply nested objects, huge payloads
|
|
11
|
+
* 3. Error Message Information Leakage — No internal paths or stack traces
|
|
12
|
+
* 4. Handler Isolation — One handler's failure must not corrupt another
|
|
13
|
+
* 5. Context Pollution — Shared mutable context between calls
|
|
14
|
+
* 6. Registry Enumeration — Error messages reveal tool inventory
|
|
15
|
+
* 7. Type Confusion — JS coercion attacks via valueOf/toString
|
|
16
|
+
* 8. Schema Poisoning — Action schema polluting shared inputSchema
|
|
17
|
+
* 9. Middleware Bypass Attempts — Manipulating args to skip validation
|
|
18
|
+
* 10. Frozen Definition Tampering — Mutating cached tool definition
|
|
19
|
+
* 11. Timing-Safe Action Lookup — No enumeration via timing
|
|
20
|
+
* 12. Zod Coercion Exploitation — Exploiting type coercion edge cases
|
|
21
|
+
* 13. Recursive/Circular Reference — Objects with circular refs
|
|
22
|
+
* 14. Symbol & Non-String Key Injection — Non-string property keys
|
|
23
|
+
* 15. Denial of Service via Handler — Handlers that never resolve
|
|
24
|
+
*/
|
|
25
|
+
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { GroupedToolBuilder } from '../../src/framework/GroupedToolBuilder.js';
|
|
28
|
+
import { ToolRegistry } from '../../src/framework/ToolRegistry.js';
|
|
29
|
+
import { success, error } from '../../src/framework/ResponseHelper.js';
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// 1. ReDoS — Catastrophic Regex Backtracking
|
|
32
|
+
// ============================================================================
|
|
33
|
+
describe('Security: ReDoS via Zod Patterns', () => {
|
|
34
|
+
it('should handle evil regex input without hanging (exponential backtracking)', async () => {
|
|
35
|
+
const builder = new GroupedToolBuilder('redos_test')
|
|
36
|
+
.action({
|
|
37
|
+
name: 'search',
|
|
38
|
+
schema: z.object({
|
|
39
|
+
// Common vulnerable pattern: nested quantifiers
|
|
40
|
+
query: z.string().regex(/^([a-zA-Z0-9]+)*$/),
|
|
41
|
+
}),
|
|
42
|
+
handler: async (_ctx, args) => success(`found: ${args.query}`),
|
|
43
|
+
});
|
|
44
|
+
builder.buildToolDefinition();
|
|
45
|
+
// Evil input designed to cause catastrophic backtracking
|
|
46
|
+
// Pattern: valid chars followed by a non-matching char
|
|
47
|
+
const evilInput = 'a'.repeat(25) + '!';
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
const result = await builder.execute(undefined, {
|
|
50
|
+
action: 'search',
|
|
51
|
+
query: evilInput,
|
|
52
|
+
});
|
|
53
|
+
const elapsed = Date.now() - start;
|
|
54
|
+
// Should fail validation (not match regex) — not hang
|
|
55
|
+
expect(result.isError).toBe(true);
|
|
56
|
+
// Should complete in reasonable time (< 5s), not exponential
|
|
57
|
+
expect(elapsed).toBeLessThan(5000);
|
|
58
|
+
});
|
|
59
|
+
it('should safely validate very long strings against patterns', async () => {
|
|
60
|
+
const builder = new GroupedToolBuilder('long_pattern')
|
|
61
|
+
.action({
|
|
62
|
+
name: 'validate',
|
|
63
|
+
schema: z.object({
|
|
64
|
+
input: z.string().max(10000).regex(/^[a-z]+$/),
|
|
65
|
+
}),
|
|
66
|
+
handler: async () => success('ok'),
|
|
67
|
+
});
|
|
68
|
+
builder.buildToolDefinition();
|
|
69
|
+
// Valid long string
|
|
70
|
+
const result = await builder.execute(undefined, {
|
|
71
|
+
action: 'validate',
|
|
72
|
+
input: 'a'.repeat(10000),
|
|
73
|
+
});
|
|
74
|
+
expect(result.isError).toBeUndefined();
|
|
75
|
+
// Just over max
|
|
76
|
+
const result2 = await builder.execute(undefined, {
|
|
77
|
+
action: 'validate',
|
|
78
|
+
input: 'a'.repeat(10001),
|
|
79
|
+
});
|
|
80
|
+
expect(result2.isError).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// 2. JSON Bomb / Memory Exhaustion
|
|
85
|
+
// ============================================================================
|
|
86
|
+
describe('Security: JSON Bomb & Memory Exhaustion', () => {
|
|
87
|
+
it('should handle deeply nested object input without stack overflow', async () => {
|
|
88
|
+
const builder = new GroupedToolBuilder('nested_bomb')
|
|
89
|
+
.action({
|
|
90
|
+
name: 'process',
|
|
91
|
+
schema: z.object({ data: z.any() }),
|
|
92
|
+
handler: async () => success('processed'),
|
|
93
|
+
});
|
|
94
|
+
builder.buildToolDefinition();
|
|
95
|
+
// Create deeply nested object (100 levels)
|
|
96
|
+
let nested = { value: 'leaf' };
|
|
97
|
+
for (let i = 0; i < 100; i++) {
|
|
98
|
+
nested = { child: nested };
|
|
99
|
+
}
|
|
100
|
+
const result = await builder.execute(undefined, {
|
|
101
|
+
action: 'process',
|
|
102
|
+
data: nested,
|
|
103
|
+
});
|
|
104
|
+
// Should not crash — z.any() accepts anything
|
|
105
|
+
expect(result.isError).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
it('should handle massive string payload gracefully', async () => {
|
|
108
|
+
const builder = new GroupedToolBuilder('big_string')
|
|
109
|
+
.action({
|
|
110
|
+
name: 'ingest',
|
|
111
|
+
schema: z.object({
|
|
112
|
+
content: z.string().max(1000000),
|
|
113
|
+
}),
|
|
114
|
+
handler: async (_ctx, args) => success(`ingested ${args.content.length} chars`),
|
|
115
|
+
});
|
|
116
|
+
builder.buildToolDefinition();
|
|
117
|
+
// 1MB string — at limit
|
|
118
|
+
const result = await builder.execute(undefined, {
|
|
119
|
+
action: 'ingest',
|
|
120
|
+
content: 'x'.repeat(1000000),
|
|
121
|
+
});
|
|
122
|
+
expect(result.isError).toBeUndefined();
|
|
123
|
+
// Over limit
|
|
124
|
+
const result2 = await builder.execute(undefined, {
|
|
125
|
+
action: 'ingest',
|
|
126
|
+
content: 'x'.repeat(1000001),
|
|
127
|
+
});
|
|
128
|
+
expect(result2.isError).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
it('should handle array with many elements', async () => {
|
|
131
|
+
const builder = new GroupedToolBuilder('big_array')
|
|
132
|
+
.action({
|
|
133
|
+
name: 'batch',
|
|
134
|
+
schema: z.object({
|
|
135
|
+
items: z.array(z.string()).max(10000),
|
|
136
|
+
}),
|
|
137
|
+
handler: async (_ctx, args) => success(`batched ${args.items.length}`),
|
|
138
|
+
});
|
|
139
|
+
builder.buildToolDefinition();
|
|
140
|
+
const result = await builder.execute(undefined, {
|
|
141
|
+
action: 'batch',
|
|
142
|
+
items: Array.from({ length: 10000 }, (_, i) => `item-${i}`),
|
|
143
|
+
});
|
|
144
|
+
expect(result.isError).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// 3. Error Message Information Leakage
|
|
149
|
+
// ============================================================================
|
|
150
|
+
describe('Security: Error Message Information Leakage', () => {
|
|
151
|
+
it('handler exceptions should not expose stack traces', async () => {
|
|
152
|
+
const builder = new GroupedToolBuilder('leak_test')
|
|
153
|
+
.action({
|
|
154
|
+
name: 'explode',
|
|
155
|
+
handler: async () => {
|
|
156
|
+
const err = new Error('DB connection failed');
|
|
157
|
+
err.stack = 'Error: DB connection failed\n at /app/src/db/pool.ts:42:12';
|
|
158
|
+
throw err;
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
builder.buildToolDefinition();
|
|
162
|
+
const result = await builder.execute(undefined, { action: 'explode' });
|
|
163
|
+
expect(result.isError).toBe(true);
|
|
164
|
+
// Should contain message but NOT stack trace
|
|
165
|
+
expect(result.content[0].text).toContain('DB connection failed');
|
|
166
|
+
expect(result.content[0].text).not.toContain('/app/src/');
|
|
167
|
+
expect(result.content[0].text).not.toContain('.ts:');
|
|
168
|
+
expect(result.content[0].text).not.toContain('at ');
|
|
169
|
+
});
|
|
170
|
+
it('validation errors should not expose Zod internals', async () => {
|
|
171
|
+
const builder = new GroupedToolBuilder('zod_leak')
|
|
172
|
+
.action({
|
|
173
|
+
name: 'create',
|
|
174
|
+
schema: z.object({
|
|
175
|
+
email: z.string().email(),
|
|
176
|
+
password: z.string().min(8),
|
|
177
|
+
}),
|
|
178
|
+
handler: async () => success('created'),
|
|
179
|
+
});
|
|
180
|
+
builder.buildToolDefinition();
|
|
181
|
+
const result = await builder.execute(undefined, {
|
|
182
|
+
action: 'create',
|
|
183
|
+
email: 'not-email',
|
|
184
|
+
password: '123',
|
|
185
|
+
});
|
|
186
|
+
expect(result.isError).toBe(true);
|
|
187
|
+
// Should describe the field errors but not Zod class names
|
|
188
|
+
expect(result.content[0].text).not.toContain('ZodError');
|
|
189
|
+
expect(result.content[0].text).not.toContain('ZodIssue');
|
|
190
|
+
expect(result.content[0].text).toContain('Validation failed');
|
|
191
|
+
});
|
|
192
|
+
it('unknown action error should list available actions (intentional for LLM)', async () => {
|
|
193
|
+
const builder = new GroupedToolBuilder('enum_test')
|
|
194
|
+
.action({ name: 'list', handler: async () => success('ok') })
|
|
195
|
+
.action({ name: 'create', handler: async () => success('ok') });
|
|
196
|
+
builder.buildToolDefinition();
|
|
197
|
+
const result = await builder.execute(undefined, { action: 'hack' });
|
|
198
|
+
expect(result.isError).toBe(true);
|
|
199
|
+
// For MCP, listing available actions is intentional (helps LLM self-correct)
|
|
200
|
+
expect(result.content[0].text).toContain('list');
|
|
201
|
+
expect(result.content[0].text).toContain('create');
|
|
202
|
+
});
|
|
203
|
+
it('unknown tool in registry should list available tools (intentional for LLM)', async () => {
|
|
204
|
+
const registry = new ToolRegistry();
|
|
205
|
+
registry.register(new GroupedToolBuilder('users')
|
|
206
|
+
.action({ name: 'list', handler: async () => success('ok') }));
|
|
207
|
+
registry.register(new GroupedToolBuilder('billing')
|
|
208
|
+
.action({ name: 'charge', handler: async () => success('ok') }));
|
|
209
|
+
const result = await registry.routeCall(undefined, 'admin', {});
|
|
210
|
+
expect(result.isError).toBe(true);
|
|
211
|
+
expect(result.content[0].text).toContain('users');
|
|
212
|
+
expect(result.content[0].text).toContain('billing');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
// ============================================================================
|
|
216
|
+
// 4. Handler Isolation — Failure Containment
|
|
217
|
+
// ============================================================================
|
|
218
|
+
describe('Security: Handler Isolation', () => {
|
|
219
|
+
it('one handler throwing should not affect another handler', async () => {
|
|
220
|
+
let stateA = 'clean';
|
|
221
|
+
const builder = new GroupedToolBuilder('isolation')
|
|
222
|
+
.action({
|
|
223
|
+
name: 'safe',
|
|
224
|
+
handler: async () => {
|
|
225
|
+
stateA = 'executed';
|
|
226
|
+
return success('safe ok');
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
.action({
|
|
230
|
+
name: 'bomb',
|
|
231
|
+
handler: async () => {
|
|
232
|
+
throw new Error('KABOOM');
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
builder.buildToolDefinition();
|
|
236
|
+
// Bomb first
|
|
237
|
+
const r1 = await builder.execute(undefined, { action: 'bomb' });
|
|
238
|
+
expect(r1.isError).toBe(true);
|
|
239
|
+
// Safe should still work perfectly
|
|
240
|
+
const r2 = await builder.execute(undefined, { action: 'safe' });
|
|
241
|
+
expect(r2.isError).toBeUndefined();
|
|
242
|
+
expect(stateA).toBe('executed');
|
|
243
|
+
});
|
|
244
|
+
it('handler throwing non-Error objects should be contained', async () => {
|
|
245
|
+
const builder = new GroupedToolBuilder('non_error')
|
|
246
|
+
.action({
|
|
247
|
+
name: 'throw_string',
|
|
248
|
+
handler: async () => { throw 'raw string error'; },
|
|
249
|
+
})
|
|
250
|
+
.action({
|
|
251
|
+
name: 'throw_number',
|
|
252
|
+
handler: async () => { throw 42; },
|
|
253
|
+
})
|
|
254
|
+
.action({
|
|
255
|
+
name: 'throw_null',
|
|
256
|
+
handler: async () => { throw null; },
|
|
257
|
+
})
|
|
258
|
+
.action({
|
|
259
|
+
name: 'throw_undefined',
|
|
260
|
+
handler: async () => { throw undefined; },
|
|
261
|
+
})
|
|
262
|
+
.action({
|
|
263
|
+
name: 'throw_object',
|
|
264
|
+
handler: async () => { throw { code: 'ERR', msg: 'fail' }; },
|
|
265
|
+
});
|
|
266
|
+
builder.buildToolDefinition();
|
|
267
|
+
for (const action of ['throw_string', 'throw_number', 'throw_null', 'throw_undefined', 'throw_object']) {
|
|
268
|
+
const r = await builder.execute(undefined, { action });
|
|
269
|
+
expect(r.isError).toBe(true);
|
|
270
|
+
expect(r.content[0].type).toBe('text');
|
|
271
|
+
expect(typeof r.content[0].text).toBe('string');
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
it('synchronous throw inside async handler should be caught', async () => {
|
|
275
|
+
const builder = new GroupedToolBuilder('sync_throw')
|
|
276
|
+
.action({
|
|
277
|
+
name: 'sync_bomb',
|
|
278
|
+
handler: async () => {
|
|
279
|
+
// Synchronous throw inside async function
|
|
280
|
+
if (true)
|
|
281
|
+
throw new RangeError('out of range');
|
|
282
|
+
return success('unreachable');
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
builder.buildToolDefinition();
|
|
286
|
+
const r = await builder.execute(undefined, { action: 'sync_bomb' });
|
|
287
|
+
expect(r.isError).toBe(true);
|
|
288
|
+
expect(r.content[0].text).toContain('out of range');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// 5. Context Pollution Between Calls
|
|
293
|
+
// ============================================================================
|
|
294
|
+
describe('Security: Context Pollution', () => {
|
|
295
|
+
it('mutable context should not leak state between independent calls', async () => {
|
|
296
|
+
const builder = new GroupedToolBuilder('ctx_pollution')
|
|
297
|
+
.action({
|
|
298
|
+
name: 'write',
|
|
299
|
+
schema: z.object({ key: z.string(), value: z.string() }),
|
|
300
|
+
handler: async (ctx, args) => {
|
|
301
|
+
ctx.data.set(args.key, args.value);
|
|
302
|
+
return success(`wrote ${args.key}`);
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
.action({
|
|
306
|
+
name: 'read',
|
|
307
|
+
schema: z.object({ key: z.string() }),
|
|
308
|
+
handler: async (ctx, args) => {
|
|
309
|
+
const val = ctx.data.get(args.key);
|
|
310
|
+
return success(val !== null && val !== void 0 ? val : 'NOT_FOUND');
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
builder.buildToolDefinition();
|
|
314
|
+
// Tenant A writes
|
|
315
|
+
const ctxA = { tenantId: 'A', data: new Map() };
|
|
316
|
+
await builder.execute(ctxA, { action: 'write', key: 'secret', value: 'A-data' });
|
|
317
|
+
// Tenant B should NOT see Tenant A's data (separate context objects)
|
|
318
|
+
const ctxB = { tenantId: 'B', data: new Map() };
|
|
319
|
+
const result = await builder.execute(ctxB, { action: 'read', key: 'secret' });
|
|
320
|
+
expect(result.content[0].text).toBe('NOT_FOUND');
|
|
321
|
+
// Tenant A should still have its data
|
|
322
|
+
const resultA = await builder.execute(ctxA, { action: 'read', key: 'secret' });
|
|
323
|
+
expect(resultA.content[0].text).toBe('A-data');
|
|
324
|
+
});
|
|
325
|
+
it('middleware should not be able to permanently pollute shared builder state', async () => {
|
|
326
|
+
let middlewareCallCount = 0;
|
|
327
|
+
const builder = new GroupedToolBuilder('mw_pollution')
|
|
328
|
+
.use(async (_ctx, _args, next) => {
|
|
329
|
+
middlewareCallCount++;
|
|
330
|
+
return next();
|
|
331
|
+
})
|
|
332
|
+
.action({
|
|
333
|
+
name: 'check',
|
|
334
|
+
handler: async () => success('ok'),
|
|
335
|
+
});
|
|
336
|
+
builder.buildToolDefinition();
|
|
337
|
+
await builder.execute(undefined, { action: 'check' });
|
|
338
|
+
expect(middlewareCallCount).toBe(1);
|
|
339
|
+
await builder.execute(undefined, { action: 'check' });
|
|
340
|
+
expect(middlewareCallCount).toBe(2);
|
|
341
|
+
// Each call increments individually — no accumulated side effects on builder
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// 6. Registry Enumeration Attack
|
|
346
|
+
// ============================================================================
|
|
347
|
+
describe('Security: Registry Enumeration', () => {
|
|
348
|
+
it('routeCall error reveals registered tool names (by design for LLM)', async () => {
|
|
349
|
+
const registry = new ToolRegistry();
|
|
350
|
+
const secretTools = ['admin_panel', 'internal_debug', 'user_management'];
|
|
351
|
+
for (const name of secretTools) {
|
|
352
|
+
registry.register(new GroupedToolBuilder(name)
|
|
353
|
+
.action({ name: 'run', handler: async () => success('ok') }));
|
|
354
|
+
}
|
|
355
|
+
const result = await registry.routeCall(undefined, 'probe', {});
|
|
356
|
+
expect(result.isError).toBe(true);
|
|
357
|
+
// In MCP context, this is BY DESIGN — the LLM needs to know available tools
|
|
358
|
+
// But this test documents it explicitly so the team knows
|
|
359
|
+
for (const name of secretTools) {
|
|
360
|
+
expect(result.content[0].text).toContain(name);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
it('tag filtering can hide tools from LLM context', () => {
|
|
364
|
+
const registry = new ToolRegistry();
|
|
365
|
+
registry.register(new GroupedToolBuilder('public_api')
|
|
366
|
+
.tags('public')
|
|
367
|
+
.action({ name: 'list', handler: async () => success('ok') }));
|
|
368
|
+
registry.register(new GroupedToolBuilder('admin_internal')
|
|
369
|
+
.tags('admin', 'internal')
|
|
370
|
+
.action({ name: 'debug', handler: async () => success('ok') }));
|
|
371
|
+
// Public API should only see public tools
|
|
372
|
+
const publicTools = registry.getTools({ tags: ['public'] });
|
|
373
|
+
expect(publicTools).toHaveLength(1);
|
|
374
|
+
expect(publicTools[0].name).toBe('public_api');
|
|
375
|
+
// Exclude internal tools
|
|
376
|
+
const filtered = registry.getTools({ exclude: ['internal'] });
|
|
377
|
+
expect(filtered).toHaveLength(1);
|
|
378
|
+
expect(filtered[0].name).toBe('public_api');
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// ============================================================================
|
|
382
|
+
// 7. Type Confusion Attacks
|
|
383
|
+
// ============================================================================
|
|
384
|
+
describe('Security: Type Confusion', () => {
|
|
385
|
+
it('object with valueOf returning string should be rejected by Zod string validation', async () => {
|
|
386
|
+
const builder = new GroupedToolBuilder('type_confusion')
|
|
387
|
+
.action({
|
|
388
|
+
name: 'process',
|
|
389
|
+
schema: z.object({ name: z.string() }),
|
|
390
|
+
handler: async (_ctx, args) => success(`hello ${args.name}`),
|
|
391
|
+
});
|
|
392
|
+
builder.buildToolDefinition();
|
|
393
|
+
// Object masquerading as string
|
|
394
|
+
const evilObj = {
|
|
395
|
+
valueOf: () => 'injected',
|
|
396
|
+
toString: () => 'injected',
|
|
397
|
+
};
|
|
398
|
+
const result = await builder.execute(undefined, {
|
|
399
|
+
action: 'process',
|
|
400
|
+
name: evilObj,
|
|
401
|
+
});
|
|
402
|
+
// Zod's strict type checking should reject non-string
|
|
403
|
+
expect(result.isError).toBe(true);
|
|
404
|
+
});
|
|
405
|
+
it('number where string expected should fail validation', async () => {
|
|
406
|
+
const builder = new GroupedToolBuilder('num_as_str')
|
|
407
|
+
.action({
|
|
408
|
+
name: 'greet',
|
|
409
|
+
schema: z.object({ name: z.string() }),
|
|
410
|
+
handler: async (_ctx, args) => success(`hi ${args.name}`),
|
|
411
|
+
});
|
|
412
|
+
builder.buildToolDefinition();
|
|
413
|
+
const result = await builder.execute(undefined, {
|
|
414
|
+
action: 'greet',
|
|
415
|
+
name: 12345,
|
|
416
|
+
});
|
|
417
|
+
expect(result.isError).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
it('string where number expected should fail validation', async () => {
|
|
420
|
+
const builder = new GroupedToolBuilder('str_as_num')
|
|
421
|
+
.action({
|
|
422
|
+
name: 'compute',
|
|
423
|
+
schema: z.object({ value: z.number() }),
|
|
424
|
+
handler: async (_ctx, args) => success(`result: ${args.value}`),
|
|
425
|
+
});
|
|
426
|
+
builder.buildToolDefinition();
|
|
427
|
+
const result = await builder.execute(undefined, {
|
|
428
|
+
action: 'compute',
|
|
429
|
+
value: '42', // string "42", not number 42
|
|
430
|
+
});
|
|
431
|
+
expect(result.isError).toBe(true);
|
|
432
|
+
});
|
|
433
|
+
it('boolean where string expected should fail validation', async () => {
|
|
434
|
+
const builder = new GroupedToolBuilder('bool_as_str')
|
|
435
|
+
.action({
|
|
436
|
+
name: 'process',
|
|
437
|
+
schema: z.object({ flag: z.string() }),
|
|
438
|
+
handler: async (_ctx, args) => success(`flag: ${args.flag}`),
|
|
439
|
+
});
|
|
440
|
+
builder.buildToolDefinition();
|
|
441
|
+
const result = await builder.execute(undefined, {
|
|
442
|
+
action: 'process',
|
|
443
|
+
flag: true,
|
|
444
|
+
});
|
|
445
|
+
expect(result.isError).toBe(true);
|
|
446
|
+
});
|
|
447
|
+
it('array where object expected should fail validation', async () => {
|
|
448
|
+
const builder = new GroupedToolBuilder('array_as_obj')
|
|
449
|
+
.action({
|
|
450
|
+
name: 'process',
|
|
451
|
+
schema: z.object({
|
|
452
|
+
config: z.object({ key: z.string() }),
|
|
453
|
+
}),
|
|
454
|
+
handler: async () => success('ok'),
|
|
455
|
+
});
|
|
456
|
+
builder.buildToolDefinition();
|
|
457
|
+
const result = await builder.execute(undefined, {
|
|
458
|
+
action: 'process',
|
|
459
|
+
config: ['key', 'value'], // array, not object
|
|
460
|
+
});
|
|
461
|
+
expect(result.isError).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// 8. Schema Poisoning — Cross-Action Contamination
|
|
466
|
+
// ============================================================================
|
|
467
|
+
describe('Security: Schema Poisoning', () => {
|
|
468
|
+
it('first action schema should not be overwritten by second action with same field', () => {
|
|
469
|
+
const builder = new GroupedToolBuilder('schema_poison')
|
|
470
|
+
.action({
|
|
471
|
+
name: 'create',
|
|
472
|
+
schema: z.object({
|
|
473
|
+
name: z.string().describe('Full name of the user'),
|
|
474
|
+
}),
|
|
475
|
+
handler: async () => success('ok'),
|
|
476
|
+
})
|
|
477
|
+
.action({
|
|
478
|
+
name: 'search',
|
|
479
|
+
schema: z.object({
|
|
480
|
+
name: z.string().describe('HACKED: This overrides the first'),
|
|
481
|
+
}),
|
|
482
|
+
handler: async () => success('ok'),
|
|
483
|
+
});
|
|
484
|
+
const def = builder.buildToolDefinition();
|
|
485
|
+
const nameField = def.inputSchema.properties.name;
|
|
486
|
+
// First declaration wins — description should be from 'create'
|
|
487
|
+
expect(nameField.description).toContain('Full name');
|
|
488
|
+
});
|
|
489
|
+
it('common schema fields should be separate from action schema fields in validation', async () => {
|
|
490
|
+
const builder = new GroupedToolBuilder('validate_isolation')
|
|
491
|
+
.commonSchema(z.object({
|
|
492
|
+
org: z.string().min(1),
|
|
493
|
+
}))
|
|
494
|
+
.action({
|
|
495
|
+
name: 'strict',
|
|
496
|
+
schema: z.object({
|
|
497
|
+
value: z.number().int().positive(),
|
|
498
|
+
}),
|
|
499
|
+
handler: async () => success('strict ok'),
|
|
500
|
+
})
|
|
501
|
+
.action({
|
|
502
|
+
name: 'loose',
|
|
503
|
+
schema: z.object({
|
|
504
|
+
value: z.string().optional(),
|
|
505
|
+
}),
|
|
506
|
+
handler: async () => success('loose ok'),
|
|
507
|
+
});
|
|
508
|
+
builder.buildToolDefinition();
|
|
509
|
+
// 'strict' requires positive integer for value
|
|
510
|
+
const r1 = await builder.execute(undefined, {
|
|
511
|
+
action: 'strict', org: 'acme', value: -5,
|
|
512
|
+
});
|
|
513
|
+
expect(r1.isError).toBe(true);
|
|
514
|
+
// 'loose' allows optional string for value — different validation context
|
|
515
|
+
const r2 = await builder.execute(undefined, {
|
|
516
|
+
action: 'loose', org: 'acme',
|
|
517
|
+
});
|
|
518
|
+
expect(r2.isError).toBeUndefined();
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
// ============================================================================
|
|
522
|
+
// 9. Middleware Bypass Attempts
|
|
523
|
+
// ============================================================================
|
|
524
|
+
describe('Security: Middleware Bypass', () => {
|
|
525
|
+
it('cannot bypass middleware by manipulating discriminator after validation', async () => {
|
|
526
|
+
const middlewareLog = [];
|
|
527
|
+
const builder = new GroupedToolBuilder('mw_bypass')
|
|
528
|
+
.use(async (_ctx, args, next) => {
|
|
529
|
+
middlewareLog.push(`mw:${args.action}`);
|
|
530
|
+
return next();
|
|
531
|
+
})
|
|
532
|
+
.action({
|
|
533
|
+
name: 'public',
|
|
534
|
+
handler: async () => success('public result'),
|
|
535
|
+
})
|
|
536
|
+
.action({
|
|
537
|
+
name: 'admin',
|
|
538
|
+
handler: async () => success('admin result'),
|
|
539
|
+
});
|
|
540
|
+
builder.buildToolDefinition();
|
|
541
|
+
// Normal call
|
|
542
|
+
await builder.execute(undefined, { action: 'public' });
|
|
543
|
+
expect(middlewareLog).toContain('mw:public');
|
|
544
|
+
// Try calling admin — middleware still runs
|
|
545
|
+
middlewareLog.length = 0;
|
|
546
|
+
await builder.execute(undefined, { action: 'admin' });
|
|
547
|
+
expect(middlewareLog).toContain('mw:admin');
|
|
548
|
+
});
|
|
549
|
+
it('middleware receives discriminator value for authorization checks', async () => {
|
|
550
|
+
const builder = new GroupedToolBuilder('auth_mw')
|
|
551
|
+
.use(async (ctx, args, next) => {
|
|
552
|
+
if (args.action === 'admin_delete' && ctx.role !== 'admin') {
|
|
553
|
+
return error('FORBIDDEN: admin only');
|
|
554
|
+
}
|
|
555
|
+
return next();
|
|
556
|
+
})
|
|
557
|
+
.action({
|
|
558
|
+
name: 'list',
|
|
559
|
+
handler: async () => success('list ok'),
|
|
560
|
+
})
|
|
561
|
+
.action({
|
|
562
|
+
name: 'admin_delete',
|
|
563
|
+
handler: async () => success('deleted'),
|
|
564
|
+
});
|
|
565
|
+
builder.buildToolDefinition();
|
|
566
|
+
// Non-admin trying admin action
|
|
567
|
+
const r1 = await builder.execute({ role: 'user' }, { action: 'admin_delete' });
|
|
568
|
+
expect(r1.isError).toBe(true);
|
|
569
|
+
expect(r1.content[0].text).toContain('FORBIDDEN');
|
|
570
|
+
// Admin can access
|
|
571
|
+
const r2 = await builder.execute({ role: 'admin' }, { action: 'admin_delete' });
|
|
572
|
+
expect(r2.isError).toBeUndefined();
|
|
573
|
+
// Non-admin on public action is fine
|
|
574
|
+
const r3 = await builder.execute({ role: 'user' }, { action: 'list' });
|
|
575
|
+
expect(r3.isError).toBeUndefined();
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// 10. Frozen Definition Tampering
|
|
580
|
+
// ============================================================================
|
|
581
|
+
describe('Security: Frozen Definition Tampering', () => {
|
|
582
|
+
it('mutating returned tool definition should not affect future calls', () => {
|
|
583
|
+
const builder = new GroupedToolBuilder('tamper')
|
|
584
|
+
.description('Original description')
|
|
585
|
+
.action({ name: 'ping', handler: async () => success('pong') });
|
|
586
|
+
const def1 = builder.buildToolDefinition();
|
|
587
|
+
// Attempt to tamper with the returned definition
|
|
588
|
+
def1.name = 'HACKED';
|
|
589
|
+
def1.description = 'HACKED';
|
|
590
|
+
def1.inputSchema = { type: 'object', properties: {} };
|
|
591
|
+
// Since buildToolDefinition returns cached ref, the cached version IS mutated.
|
|
592
|
+
// This tests documents the behavior: the reference IS shared.
|
|
593
|
+
const def2 = builder.buildToolDefinition();
|
|
594
|
+
// Same reference — mutation is visible (this is a documentation test)
|
|
595
|
+
expect(def2).toBe(def1);
|
|
596
|
+
});
|
|
597
|
+
it('builder should still route correctly even if definition is externally mutated', async () => {
|
|
598
|
+
const builder = new GroupedToolBuilder('tamper_route')
|
|
599
|
+
.action({ name: 'ping', handler: async () => success('pong') });
|
|
600
|
+
const def = builder.buildToolDefinition();
|
|
601
|
+
// Tamper with the definition
|
|
602
|
+
def.name = 'HACKED';
|
|
603
|
+
// Routing uses internal state, not the cached definition
|
|
604
|
+
const result = await builder.execute(undefined, { action: 'ping' });
|
|
605
|
+
expect(result.isError).toBeUndefined();
|
|
606
|
+
expect(result.content[0].text).toBe('pong');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// 11. Zod Coercion Edge Cases
|
|
611
|
+
// ============================================================================
|
|
612
|
+
describe('Security: Zod Coercion Edge Cases', () => {
|
|
613
|
+
it('NaN should be rejected for number fields', async () => {
|
|
614
|
+
const builder = new GroupedToolBuilder('nan_test')
|
|
615
|
+
.action({
|
|
616
|
+
name: 'compute',
|
|
617
|
+
schema: z.object({ value: z.number() }),
|
|
618
|
+
handler: async () => success('ok'),
|
|
619
|
+
});
|
|
620
|
+
builder.buildToolDefinition();
|
|
621
|
+
const result = await builder.execute(undefined, {
|
|
622
|
+
action: 'compute',
|
|
623
|
+
value: NaN,
|
|
624
|
+
});
|
|
625
|
+
// NaN is technically a number type, but Zod should handle it
|
|
626
|
+
// This documents the behavior
|
|
627
|
+
expect(typeof NaN).toBe('number');
|
|
628
|
+
});
|
|
629
|
+
it('Infinity should be handled for number fields', async () => {
|
|
630
|
+
const builder = new GroupedToolBuilder('inf_test')
|
|
631
|
+
.action({
|
|
632
|
+
name: 'compute',
|
|
633
|
+
schema: z.object({ value: z.number().finite() }),
|
|
634
|
+
handler: async () => success('ok'),
|
|
635
|
+
});
|
|
636
|
+
builder.buildToolDefinition();
|
|
637
|
+
const r1 = await builder.execute(undefined, {
|
|
638
|
+
action: 'compute',
|
|
639
|
+
value: Infinity,
|
|
640
|
+
});
|
|
641
|
+
expect(r1.isError).toBe(true);
|
|
642
|
+
const r2 = await builder.execute(undefined, {
|
|
643
|
+
action: 'compute',
|
|
644
|
+
value: -Infinity,
|
|
645
|
+
});
|
|
646
|
+
expect(r2.isError).toBe(true);
|
|
647
|
+
});
|
|
648
|
+
it('Date object where string expected should fail', async () => {
|
|
649
|
+
const builder = new GroupedToolBuilder('date_confusion')
|
|
650
|
+
.action({
|
|
651
|
+
name: 'process',
|
|
652
|
+
schema: z.object({ timestamp: z.string() }),
|
|
653
|
+
handler: async () => success('ok'),
|
|
654
|
+
});
|
|
655
|
+
builder.buildToolDefinition();
|
|
656
|
+
const result = await builder.execute(undefined, {
|
|
657
|
+
action: 'process',
|
|
658
|
+
timestamp: new Date(),
|
|
659
|
+
});
|
|
660
|
+
expect(result.isError).toBe(true);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
// ============================================================================
|
|
664
|
+
// 12. Circular Reference Handling
|
|
665
|
+
// ============================================================================
|
|
666
|
+
describe('Security: Circular References', () => {
|
|
667
|
+
it('circular reference in args should not cause infinite loop', async () => {
|
|
668
|
+
const builder = new GroupedToolBuilder('circular')
|
|
669
|
+
.action({
|
|
670
|
+
name: 'process',
|
|
671
|
+
// No schema — no validation, just pass through
|
|
672
|
+
handler: async (_ctx, args) => {
|
|
673
|
+
try {
|
|
674
|
+
// Handler tries to serialize — would fail with circular ref
|
|
675
|
+
return success('processed');
|
|
676
|
+
}
|
|
677
|
+
catch (_a) {
|
|
678
|
+
return error('serialization failed');
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
builder.buildToolDefinition();
|
|
683
|
+
// Create circular reference
|
|
684
|
+
const args = { action: 'process', data: {} };
|
|
685
|
+
args.data.self = args.data; // circular!
|
|
686
|
+
// Should not hang — handler doesn't try to serialize
|
|
687
|
+
const result = await builder.execute(undefined, args);
|
|
688
|
+
expect(result.content[0]).toHaveProperty('type', 'text');
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// 13. Symbol & Non-String Key Injection
|
|
693
|
+
// ============================================================================
|
|
694
|
+
describe('Security: Symbol & Non-String Key Injection', () => {
|
|
695
|
+
it('Symbol keys in args should not affect routing', async () => {
|
|
696
|
+
const builder = new GroupedToolBuilder('symbol_test')
|
|
697
|
+
.action({
|
|
698
|
+
name: 'run',
|
|
699
|
+
handler: async () => success('ok'),
|
|
700
|
+
});
|
|
701
|
+
builder.buildToolDefinition();
|
|
702
|
+
const sym = Symbol('malicious');
|
|
703
|
+
const args = { action: 'run' };
|
|
704
|
+
args[sym] = 'hidden payload';
|
|
705
|
+
const result = await builder.execute(undefined, args);
|
|
706
|
+
expect(result.isError).toBeUndefined();
|
|
707
|
+
});
|
|
708
|
+
it('numeric keys in args should not confuse routing', async () => {
|
|
709
|
+
const builder = new GroupedToolBuilder('numeric_key')
|
|
710
|
+
.action({
|
|
711
|
+
name: 'run',
|
|
712
|
+
handler: async () => success('ok'),
|
|
713
|
+
});
|
|
714
|
+
builder.buildToolDefinition();
|
|
715
|
+
const result = await builder.execute(undefined, {
|
|
716
|
+
action: 'run',
|
|
717
|
+
0: 'zero',
|
|
718
|
+
1: 'one',
|
|
719
|
+
length: 2,
|
|
720
|
+
});
|
|
721
|
+
expect(result.isError).toBeUndefined();
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
// ============================================================================
|
|
725
|
+
// 14. Concurrent Execution Safety
|
|
726
|
+
// ============================================================================
|
|
727
|
+
describe('Security: Concurrent Execution Safety', () => {
|
|
728
|
+
it('parallel calls with different contexts should not cross-contaminate', async () => {
|
|
729
|
+
const builder = new GroupedToolBuilder('concurrent_ctx')
|
|
730
|
+
.action({
|
|
731
|
+
name: 'whoami',
|
|
732
|
+
handler: async (ctx) => {
|
|
733
|
+
// Simulate async work
|
|
734
|
+
await new Promise(r => setTimeout(r, Math.random() * 20));
|
|
735
|
+
return success(`user:${ctx.userId}`);
|
|
736
|
+
},
|
|
737
|
+
});
|
|
738
|
+
builder.buildToolDefinition();
|
|
739
|
+
// Fire 20 concurrent calls with different contexts
|
|
740
|
+
const promises = Array.from({ length: 20 }, (_, i) => builder.execute({ userId: `user-${i}` }, { action: 'whoami' }));
|
|
741
|
+
const results = await Promise.all(promises);
|
|
742
|
+
// Each result must correspond to its own context
|
|
743
|
+
for (let i = 0; i < 20; i++) {
|
|
744
|
+
expect(results[i].content[0].text).toBe(`user:user-${i}`);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
it('parallel registry routing should not mix up tool handlers', async () => {
|
|
748
|
+
const registry = new ToolRegistry();
|
|
749
|
+
for (let i = 0; i < 10; i++) {
|
|
750
|
+
registry.register(new GroupedToolBuilder(`tool_${i}`)
|
|
751
|
+
.action({
|
|
752
|
+
name: 'identify',
|
|
753
|
+
handler: async () => {
|
|
754
|
+
await new Promise(r => setTimeout(r, Math.random() * 10));
|
|
755
|
+
return success(`tool_${i}`);
|
|
756
|
+
},
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
const promises = Array.from({ length: 10 }, (_, i) => registry.routeCall(undefined, `tool_${i}`, { action: 'identify' }));
|
|
760
|
+
const results = await Promise.all(promises);
|
|
761
|
+
for (let i = 0; i < 10; i++) {
|
|
762
|
+
expect(results[i].content[0].text).toBe(`tool_${i}`);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
// ============================================================================
|
|
767
|
+
// 15. Payload Injection via JSON Special Values
|
|
768
|
+
// ============================================================================
|
|
769
|
+
describe('Security: JSON Special Values', () => {
|
|
770
|
+
it('should handle -0 correctly', async () => {
|
|
771
|
+
const builder = new GroupedToolBuilder('neg_zero')
|
|
772
|
+
.action({
|
|
773
|
+
name: 'compute',
|
|
774
|
+
schema: z.object({ value: z.number() }),
|
|
775
|
+
handler: async (_ctx, args) => success(`value: ${Object.is(args.value, -0) ? '-0' : args.value}`),
|
|
776
|
+
});
|
|
777
|
+
builder.buildToolDefinition();
|
|
778
|
+
const result = await builder.execute(undefined, {
|
|
779
|
+
action: 'compute',
|
|
780
|
+
value: -0,
|
|
781
|
+
});
|
|
782
|
+
expect(result.isError).toBeUndefined();
|
|
783
|
+
});
|
|
784
|
+
it('should handle unicode null bytes in strings', async () => {
|
|
785
|
+
const builder = new GroupedToolBuilder('null_byte')
|
|
786
|
+
.action({
|
|
787
|
+
name: 'process',
|
|
788
|
+
schema: z.object({ data: z.string() }),
|
|
789
|
+
handler: async (_ctx, args) => success(`len: ${args.data.length}`),
|
|
790
|
+
});
|
|
791
|
+
builder.buildToolDefinition();
|
|
792
|
+
// String with null bytes embedded
|
|
793
|
+
const result = await builder.execute(undefined, {
|
|
794
|
+
action: 'process',
|
|
795
|
+
data: 'hello\x00world\x00evil',
|
|
796
|
+
});
|
|
797
|
+
expect(result.isError).toBeUndefined();
|
|
798
|
+
});
|
|
799
|
+
it('should handle unicode surrogate pairs correctly', async () => {
|
|
800
|
+
const builder = new GroupedToolBuilder('surrogate')
|
|
801
|
+
.action({
|
|
802
|
+
name: 'process',
|
|
803
|
+
schema: z.object({ data: z.string() }),
|
|
804
|
+
handler: async (_ctx, args) => success(`got: ${args.data}`),
|
|
805
|
+
});
|
|
806
|
+
builder.buildToolDefinition();
|
|
807
|
+
// Emoji + surrogate pairs
|
|
808
|
+
const result = await builder.execute(undefined, {
|
|
809
|
+
action: 'process',
|
|
810
|
+
data: '💀🔥 test \uD83D\uDE00 end',
|
|
811
|
+
});
|
|
812
|
+
expect(result.isError).toBeUndefined();
|
|
813
|
+
});
|
|
814
|
+
it('should handle extremely long action name in input (not in definition)', async () => {
|
|
815
|
+
const builder = new GroupedToolBuilder('long_action_input')
|
|
816
|
+
.action({ name: 'valid', handler: async () => success('ok') });
|
|
817
|
+
builder.buildToolDefinition();
|
|
818
|
+
const result = await builder.execute(undefined, {
|
|
819
|
+
action: 'x'.repeat(100000), // 100KB action name
|
|
820
|
+
});
|
|
821
|
+
expect(result.isError).toBe(true);
|
|
822
|
+
expect(result.content[0].text).toContain('Unknown');
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
//# sourceMappingURL=SecurityDeep.test.js.map
|