@mctx-ai/mcp-server 0.3.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 +21 -0
- package/dist/completion.js +214 -0
- package/dist/conversation.js +139 -0
- package/dist/index.d.ts +733 -0
- package/dist/index.js +18 -0
- package/dist/log.js +213 -0
- package/dist/progress.js +84 -0
- package/dist/sampling.js +130 -0
- package/dist/security.js +339 -0
- package/dist/server.js +876 -0
- package/dist/types.js +252 -0
- package/dist/uri.js +120 -0
- package/package.json +53 -0
- package/src/completion.js +214 -0
- package/src/conversation.js +139 -0
- package/src/index.d.ts +733 -0
- package/src/index.js +18 -0
- package/src/log.js +213 -0
- package/src/progress.js +84 -0
- package/src/sampling.js +130 -0
- package/src/security.js +339 -0
- package/src/server.js +876 -0
- package/src/types.js +252 -0
- package/src/uri.js +120 -0
package/src/security.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security Module - MCP Server Security Protections
|
|
3
|
+
*
|
|
4
|
+
* Provides comprehensive security controls:
|
|
5
|
+
* - Error sanitization (secret redaction, stack trace removal)
|
|
6
|
+
* - Request/response size limits (DoS prevention)
|
|
7
|
+
* - URI scheme validation (injection prevention)
|
|
8
|
+
* - Prototype pollution protection (object sanitization)
|
|
9
|
+
*
|
|
10
|
+
* Defense in depth: Multiple layers protect against OWASP Top 10 threats.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Secret patterns to detect and redact
|
|
14
|
+
// IMPORTANT: Order matters - more specific patterns must come before generic patterns
|
|
15
|
+
const SECRET_PATTERNS = [
|
|
16
|
+
// AWS Access Keys (AKIA + 16 alphanumeric chars)
|
|
17
|
+
{ regex: /AKIA[0-9A-Z]{16}/g, label: "REDACTED_AWS_KEY" },
|
|
18
|
+
|
|
19
|
+
// JWT tokens (three base64url segments separated by dots)
|
|
20
|
+
{
|
|
21
|
+
regex: /eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g,
|
|
22
|
+
label: "REDACTED_JWT",
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Database connection strings
|
|
26
|
+
{
|
|
27
|
+
regex: /(?:mongodb|postgres|mysql|mariadb|mssql|oracle):\/\/[^\s]+/gi,
|
|
28
|
+
label: "REDACTED_CONNECTION_STRING",
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Bearer tokens
|
|
32
|
+
{ regex: /Bearer\s+[a-zA-Z0-9_\-.]+/gi, label: "Bearer [REDACTED]" },
|
|
33
|
+
|
|
34
|
+
// GitHub tokens (personal access tokens, OAuth, app tokens, refresh tokens)
|
|
35
|
+
// MUST come before generic API key pattern (which would match "token:")
|
|
36
|
+
// GitHub tokens: gh[pors]_ prefix + 33-40 alphanumeric chars
|
|
37
|
+
// Note: gh[pors] matches ghp, gho, ghr, ghs
|
|
38
|
+
{ regex: /gh[pors]_[a-zA-Z0-9]{33,40}/g, label: "REDACTED_GITHUB_TOKEN" },
|
|
39
|
+
|
|
40
|
+
// Slack tokens (format: xoxb-numbers-alphanumeric or similar hyphen-separated patterns)
|
|
41
|
+
// MUST come before generic API key pattern (which would match "token:")
|
|
42
|
+
{
|
|
43
|
+
regex: /xox[bpas]-[a-zA-Z0-9]+-[a-zA-Z0-9-]+/g,
|
|
44
|
+
label: "REDACTED_SLACK_TOKEN",
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Private keys (RSA, EC, DSA)
|
|
48
|
+
{
|
|
49
|
+
regex:
|
|
50
|
+
/-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
|
|
51
|
+
label: "REDACTED_PRIVATE_KEY",
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// GCP API keys (AIzaSy prefix + variable length alphanumeric/special chars)
|
|
55
|
+
{ regex: /AIzaSy[0-9A-Za-z\-_]{21,}/g, label: "REDACTED_GCP_KEY" },
|
|
56
|
+
|
|
57
|
+
// Azure connection strings
|
|
58
|
+
{ regex: /AccountKey=[a-zA-Z0-9+/=]{40,}/g, label: "REDACTED_AZURE_KEY" },
|
|
59
|
+
|
|
60
|
+
// Generic API key patterns (key=, token=, secret=, password= followed by value)
|
|
61
|
+
// ReDoS mitigation: bounded whitespace quantifiers (max 10 instead of unbounded \s*)
|
|
62
|
+
// MUST come LAST to avoid matching more specific token patterns above
|
|
63
|
+
{
|
|
64
|
+
regex:
|
|
65
|
+
/(?:api[_-]?key|token|secret|password)\s{0,10}[=:]\s{0,10}['"]?([a-zA-Z0-9_.-]{16,})['"]?/gi,
|
|
66
|
+
label: (match) =>
|
|
67
|
+
match.replace(/(['"]?)[a-zA-Z0-9_.-]{16,}(['"]?)/, "$1[REDACTED]$2"),
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Dangerous URI schemes that enable injection attacks
|
|
72
|
+
const DANGEROUS_SCHEMES = ["file", "javascript", "data", "vbscript", "about"];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Sanitize error messages for safe output
|
|
76
|
+
*
|
|
77
|
+
* Security: Prevents information disclosure through error messages
|
|
78
|
+
* - Removes stack traces in production (leak implementation details)
|
|
79
|
+
* - Redacts secrets (AWS keys, JWTs, connection strings, API keys)
|
|
80
|
+
* - Preserves error context for debugging
|
|
81
|
+
*
|
|
82
|
+
* @param {Error} error - Error object to sanitize
|
|
83
|
+
* @param {boolean} isProduction - True to strip stack traces (default: true)
|
|
84
|
+
* @returns {string} Sanitized error message
|
|
85
|
+
*/
|
|
86
|
+
export function sanitizeError(error, isProduction = true) {
|
|
87
|
+
if (!error) return "Unknown error";
|
|
88
|
+
|
|
89
|
+
let message = error.message || String(error);
|
|
90
|
+
|
|
91
|
+
// Redact secrets from message
|
|
92
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
93
|
+
if (typeof pattern.label === "function") {
|
|
94
|
+
message = message.replace(pattern.regex, pattern.label);
|
|
95
|
+
} else {
|
|
96
|
+
message = message.replace(pattern.regex, `[${pattern.label}]`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// In production: strip stack traces completely
|
|
101
|
+
// In development: include stack trace but redact secrets
|
|
102
|
+
if (!isProduction && error.stack) {
|
|
103
|
+
let stack = error.stack;
|
|
104
|
+
|
|
105
|
+
// Redact secrets from stack trace
|
|
106
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
107
|
+
if (typeof pattern.label === "function") {
|
|
108
|
+
stack = stack.replace(pattern.regex, pattern.label);
|
|
109
|
+
} else {
|
|
110
|
+
stack = stack.replace(pattern.regex, `[${pattern.label}]`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return stack;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Validate request body size
|
|
122
|
+
*
|
|
123
|
+
* Security: Prevents DoS attacks via large payloads
|
|
124
|
+
* - Default 1MB limit (configurable)
|
|
125
|
+
* - Validates before parsing to avoid memory exhaustion
|
|
126
|
+
*
|
|
127
|
+
* @param {string|Object} body - Request body (JSON string or parsed object)
|
|
128
|
+
* @param {number} maxSize - Maximum size in bytes (default: 1MB)
|
|
129
|
+
* @throws {Error} If body exceeds maxSize
|
|
130
|
+
*/
|
|
131
|
+
export function validateRequestSize(body, maxSize = 1048576) {
|
|
132
|
+
if (!body) return;
|
|
133
|
+
|
|
134
|
+
const size =
|
|
135
|
+
typeof body === "string" ? body.length : JSON.stringify(body).length;
|
|
136
|
+
|
|
137
|
+
if (size > maxSize) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Request body too large: ${size} bytes (max: ${maxSize} bytes)`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate response body size
|
|
146
|
+
*
|
|
147
|
+
* Security: Prevents DoS attacks via large responses
|
|
148
|
+
* - Default 1MB limit (configurable)
|
|
149
|
+
* - Protects clients from memory exhaustion
|
|
150
|
+
*
|
|
151
|
+
* @param {string|Object} body - Response body (JSON string or object)
|
|
152
|
+
* @param {number} maxSize - Maximum size in bytes (default: 1MB)
|
|
153
|
+
* @throws {Error} If body exceeds maxSize
|
|
154
|
+
*/
|
|
155
|
+
export function validateResponseSize(body, maxSize = 1048576) {
|
|
156
|
+
if (!body) return;
|
|
157
|
+
|
|
158
|
+
const size =
|
|
159
|
+
typeof body === "string" ? body.length : JSON.stringify(body).length;
|
|
160
|
+
|
|
161
|
+
if (size > maxSize) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Response body too large: ${size} bytes (max: ${maxSize} bytes)`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validate string input length
|
|
170
|
+
*
|
|
171
|
+
* Security: Prevents DoS via excessively long strings
|
|
172
|
+
* - Default 10MB limit (configurable)
|
|
173
|
+
* - Useful for individual string fields
|
|
174
|
+
*
|
|
175
|
+
* @param {string} value - String to validate
|
|
176
|
+
* @param {number} maxLength - Maximum length in characters (default: 10MB)
|
|
177
|
+
* @throws {Error} If string exceeds maxLength
|
|
178
|
+
*/
|
|
179
|
+
export function validateStringInput(value, maxLength = 10485760) {
|
|
180
|
+
if (typeof value !== "string") return;
|
|
181
|
+
|
|
182
|
+
if (value.length > maxLength) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`String input too long: ${value.length} chars (max: ${maxLength} chars)`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validate URI scheme against allowlist
|
|
191
|
+
*
|
|
192
|
+
* Security: Prevents injection attacks via dangerous URI schemes
|
|
193
|
+
* - Blocks file://, javascript:, data:, etc. by default
|
|
194
|
+
* - Allows http:// and https:// by default
|
|
195
|
+
* - Supports custom allowlists for special cases
|
|
196
|
+
*
|
|
197
|
+
* @param {string} uri - URI to validate
|
|
198
|
+
* @param {string[]} allowedSchemes - Allowed schemes (default: ['http', 'https'])
|
|
199
|
+
* @returns {boolean} True if URI scheme is allowed
|
|
200
|
+
*/
|
|
201
|
+
export function validateUriScheme(uri, allowedSchemes = ["http", "https"]) {
|
|
202
|
+
if (!uri || typeof uri !== "string") return false;
|
|
203
|
+
|
|
204
|
+
// Extract scheme (characters before first colon)
|
|
205
|
+
const schemeMatch = uri.match(/^([a-z][a-z0-9+.-]*?):/i);
|
|
206
|
+
if (!schemeMatch) return false;
|
|
207
|
+
|
|
208
|
+
const scheme = schemeMatch[1].toLowerCase();
|
|
209
|
+
|
|
210
|
+
// Check if scheme is explicitly dangerous
|
|
211
|
+
if (DANGEROUS_SCHEMES.includes(scheme)) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check if scheme is in allowlist
|
|
216
|
+
return allowedSchemes.some((allowed) => allowed.toLowerCase() === scheme);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Canonicalize path and detect traversal attempts
|
|
221
|
+
*
|
|
222
|
+
* Security: Prevents path traversal attacks
|
|
223
|
+
* - Detects ../ sequences that escape intended directory
|
|
224
|
+
* - Handles double/triple encoding (%252e%252e%252f)
|
|
225
|
+
* - Detects null byte injection (%00 and literal \0)
|
|
226
|
+
* - Handles mixed encoding (%2e./, .%2e/)
|
|
227
|
+
* - Checks Unicode variants (\u002e\u002e/)
|
|
228
|
+
* - Normalizes path for consistent validation
|
|
229
|
+
*
|
|
230
|
+
* @param {string} uri - URI/path to canonicalize
|
|
231
|
+
* @returns {string} Canonicalized path
|
|
232
|
+
* @throws {Error} If path traversal detected
|
|
233
|
+
*/
|
|
234
|
+
export function canonicalizePath(uri) {
|
|
235
|
+
if (!uri || typeof uri !== "string") {
|
|
236
|
+
throw new Error("Invalid URI: must be a non-empty string");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Decode URI repeatedly (max 3 iterations) to catch double/triple encoding
|
|
240
|
+
let decoded = uri;
|
|
241
|
+
for (let i = 0; i < 3; i++) {
|
|
242
|
+
try {
|
|
243
|
+
const nextDecoded = decodeURIComponent(decoded);
|
|
244
|
+
if (nextDecoded === decoded) break; // No more decoding needed
|
|
245
|
+
decoded = nextDecoded;
|
|
246
|
+
} catch {
|
|
247
|
+
// Invalid URI encoding, continue with current value
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Detect null byte injection (both encoded and literal)
|
|
253
|
+
if (decoded.includes("%00") || decoded.includes("\0")) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
"Path traversal detected: null byte injection is not allowed",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Detect Unicode variants of dots and slashes
|
|
260
|
+
// \u002e = '.', \u002f = '/'
|
|
261
|
+
if (
|
|
262
|
+
decoded.includes("\u002e\u002e\u002f") ||
|
|
263
|
+
decoded.includes("\u002e\u002e/") ||
|
|
264
|
+
decoded.includes("..\u002f")
|
|
265
|
+
) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
"Path traversal detected: Unicode-encoded ../ sequences are not allowed",
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check fully decoded string for literal path traversal attempts
|
|
272
|
+
if (decoded.includes("../") || decoded.includes("..\\")) {
|
|
273
|
+
throw new Error("Path traversal detected: ../ sequences are not allowed");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Check for mixed encoding in decoded string (case-insensitive)
|
|
277
|
+
const lowerDecoded = decoded.toLowerCase();
|
|
278
|
+
if (
|
|
279
|
+
lowerDecoded.includes("%2e%2e%2f") ||
|
|
280
|
+
lowerDecoded.includes("%2e%2e/") ||
|
|
281
|
+
lowerDecoded.includes("..%2f") ||
|
|
282
|
+
lowerDecoded.includes("%2e.") ||
|
|
283
|
+
lowerDecoded.includes(".%2e")
|
|
284
|
+
) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
"Path traversal detected: encoded ../ sequences are not allowed",
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Normalize path separators
|
|
291
|
+
let normalized = decoded.replace(/\\/g, "/");
|
|
292
|
+
|
|
293
|
+
// Remove duplicate slashes
|
|
294
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
295
|
+
|
|
296
|
+
return normalized;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Sanitize input object to prevent prototype pollution
|
|
301
|
+
*
|
|
302
|
+
* Security: Prevents prototype pollution attacks
|
|
303
|
+
* - Strips __proto__, constructor, prototype keys
|
|
304
|
+
* - Deep clones to avoid mutations
|
|
305
|
+
* - Handles nested objects and arrays
|
|
306
|
+
*
|
|
307
|
+
* Reference: OWASP Prototype Pollution Prevention
|
|
308
|
+
*
|
|
309
|
+
* @param {*} obj - Object to sanitize
|
|
310
|
+
* @returns {*} Clean copy with dangerous keys removed
|
|
311
|
+
*/
|
|
312
|
+
export function sanitizeInput(obj) {
|
|
313
|
+
// Handle non-objects (primitives, null, undefined)
|
|
314
|
+
if (obj === null || typeof obj !== "object") {
|
|
315
|
+
return obj;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Handle arrays
|
|
319
|
+
if (Array.isArray(obj)) {
|
|
320
|
+
return obj.map((item) => sanitizeInput(item));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Handle objects - create clean copy
|
|
324
|
+
const sanitized = {};
|
|
325
|
+
|
|
326
|
+
for (const key in obj) {
|
|
327
|
+
// Skip prototype pollution vectors
|
|
328
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Recursively sanitize nested values
|
|
333
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
334
|
+
sanitized[key] = sanitizeInput(obj[key]);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return sanitized;
|
|
339
|
+
}
|