@purplesquirrel/guardrails-mcp-server 1.0.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.
@@ -0,0 +1,291 @@
1
+ /**
2
+ * InputValidator - Validates and sanitizes incoming requests
3
+ *
4
+ * Checks for:
5
+ * - Prompt injection attempts
6
+ * - Malicious code patterns
7
+ * - PII/sensitive data
8
+ * - Content length limits
9
+ * - Blocked keywords/patterns
10
+ */
11
+
12
+ export class InputValidator {
13
+ constructor(config = {}) {
14
+ this.config = config;
15
+
16
+ // Prompt injection patterns
17
+ this.injectionPatterns = [
18
+ /ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)/i,
19
+ /disregard\s+(all\s+)?(previous|prior|above)/i,
20
+ /forget\s+(everything|all|your)\s+(you|instructions?|rules?)/i,
21
+ /you\s+are\s+now\s+(a|an|the)\s+/i,
22
+ /pretend\s+(you\s+are|to\s+be)/i,
23
+ /act\s+as\s+(if|though|a|an)/i,
24
+ /jailbreak/i,
25
+ /DAN\s+mode/i,
26
+ /developer\s+mode\s+(enabled|on|activated)/i,
27
+ /bypass\s+(safety|security|filters?|guardrails?)/i,
28
+ /override\s+(system|safety|security)/i,
29
+ /\[SYSTEM\]|\[ADMIN\]|\[ROOT\]/i,
30
+ /```system|```admin/i,
31
+ ];
32
+
33
+ // Malicious code patterns
34
+ this.codePatterns = [
35
+ /eval\s*\(/i,
36
+ /exec\s*\(/i,
37
+ /system\s*\(/i,
38
+ /subprocess/i,
39
+ /os\.system/i,
40
+ /child_process/i,
41
+ /spawn\s*\(/i,
42
+ /import\s+os\b/i,
43
+ /require\s*\(\s*['"]child_process['"]\s*\)/i,
44
+ /__import__\s*\(/i,
45
+ /\bsh\s+-c\b/i,
46
+ /\bbash\s+-c\b/i,
47
+ /;\s*rm\s+-rf/i,
48
+ /&&\s*rm\s+-rf/i,
49
+ /\|\s*sh\b/i,
50
+ /\$\(.*\)/,
51
+ /`.*`/,
52
+ ];
53
+
54
+ // PII patterns
55
+ this.piiPatterns = [
56
+ { name: 'SSN', pattern: /\b\d{3}-\d{2}-\d{4}\b/ },
57
+ { name: 'CreditCard', pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/ },
58
+ { name: 'Email', pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g },
59
+ { name: 'Phone', pattern: /\b(?:\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/ },
60
+ { name: 'IPAddress', pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/ },
61
+ { name: 'APIKey', pattern: /\b(?:sk-|pk-|api[_-]?key[_-]?)[a-zA-Z0-9]{20,}\b/i },
62
+ { name: 'AWSKey', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
63
+ { name: 'Password', pattern: /\b(?:password|passwd|pwd)\s*[:=]\s*\S+/i },
64
+ ];
65
+
66
+ // Blocked keywords (configurable)
67
+ this.blockedKeywords = config.blockedKeywords || [];
68
+ }
69
+
70
+ /**
71
+ * Validate an incoming request
72
+ * @param {Object} request - The request to validate
73
+ * @param {Object} context - Additional context
74
+ * @returns {Object} - Validation result
75
+ */
76
+ async validate(request, context = {}) {
77
+ const violations = [];
78
+ let sanitizedRequest = JSON.parse(JSON.stringify(request));
79
+
80
+ // Extract text content from request
81
+ const textContent = this.extractTextContent(request);
82
+
83
+ // 1. Check content length
84
+ if (textContent.length > (this.config.maxInputLength || 100000)) {
85
+ violations.push({
86
+ type: 'LENGTH_EXCEEDED',
87
+ message: `Input exceeds maximum length of ${this.config.maxInputLength || 100000} characters`,
88
+ severity: 'high',
89
+ });
90
+ }
91
+
92
+ // 2. Check for prompt injection
93
+ for (const pattern of this.injectionPatterns) {
94
+ if (pattern.test(textContent)) {
95
+ violations.push({
96
+ type: 'PROMPT_INJECTION',
97
+ message: 'Potential prompt injection detected',
98
+ pattern: pattern.toString(),
99
+ severity: 'critical',
100
+ });
101
+ }
102
+ }
103
+
104
+ // 3. Check for malicious code patterns
105
+ for (const pattern of this.codePatterns) {
106
+ if (pattern.test(textContent)) {
107
+ violations.push({
108
+ type: 'MALICIOUS_CODE',
109
+ message: 'Potentially malicious code pattern detected',
110
+ pattern: pattern.toString(),
111
+ severity: 'high',
112
+ });
113
+ }
114
+ }
115
+
116
+ // 4. Check for PII (if configured to block)
117
+ if (this.config.blockPII) {
118
+ for (const pii of this.piiPatterns) {
119
+ if (pii.pattern.test(textContent)) {
120
+ violations.push({
121
+ type: 'PII_DETECTED',
122
+ message: `${pii.name} pattern detected in input`,
123
+ severity: 'medium',
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ // 5. Check blocked keywords
130
+ for (const keyword of this.blockedKeywords) {
131
+ const regex = typeof keyword === 'string'
132
+ ? new RegExp(keyword, 'i')
133
+ : keyword;
134
+ if (regex.test(textContent)) {
135
+ violations.push({
136
+ type: 'BLOCKED_KEYWORD',
137
+ message: 'Blocked keyword detected',
138
+ severity: 'high',
139
+ });
140
+ }
141
+ }
142
+
143
+ // 6. Check custom patterns from config
144
+ if (this.config.blockedPatterns) {
145
+ for (const pattern of this.config.blockedPatterns) {
146
+ const regex = typeof pattern === 'string'
147
+ ? new RegExp(pattern, 'i')
148
+ : pattern;
149
+ if (regex.test(textContent)) {
150
+ violations.push({
151
+ type: 'BLOCKED_PATTERN',
152
+ message: 'Blocked pattern detected',
153
+ severity: 'high',
154
+ });
155
+ }
156
+ }
157
+ }
158
+
159
+ // 7. Sanitize if configured
160
+ if (this.config.sanitizeInput) {
161
+ sanitizedRequest = this.sanitize(sanitizedRequest);
162
+ }
163
+
164
+ // Determine if valid based on violations
165
+ const criticalViolations = violations.filter(v => v.severity === 'critical');
166
+ const highViolations = violations.filter(v => v.severity === 'high');
167
+
168
+ const valid = criticalViolations.length === 0 &&
169
+ (this.config.allowHighSeverity || highViolations.length === 0);
170
+
171
+ return {
172
+ valid,
173
+ violations,
174
+ sanitizedRequest: valid ? sanitizedRequest : null,
175
+ stats: {
176
+ criticalCount: criticalViolations.length,
177
+ highCount: highViolations.length,
178
+ mediumCount: violations.filter(v => v.severity === 'medium').length,
179
+ },
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Extract text content from various request formats
185
+ */
186
+ extractTextContent(request) {
187
+ if (typeof request === 'string') {
188
+ return request;
189
+ }
190
+
191
+ const parts = [];
192
+
193
+ // Handle common request structures
194
+ if (request.prompt) parts.push(request.prompt);
195
+ if (request.input) parts.push(request.input);
196
+ if (request.text) parts.push(request.text);
197
+ if (request.content) parts.push(request.content);
198
+ if (request.message) parts.push(request.message);
199
+ if (request.query) parts.push(request.query);
200
+
201
+ // Handle messages array (chat format)
202
+ if (Array.isArray(request.messages)) {
203
+ for (const msg of request.messages) {
204
+ if (msg.content) parts.push(msg.content);
205
+ }
206
+ }
207
+
208
+ // Handle arguments object
209
+ if (request.arguments) {
210
+ parts.push(JSON.stringify(request.arguments));
211
+ }
212
+
213
+ return parts.join('\n');
214
+ }
215
+
216
+ /**
217
+ * Sanitize request content
218
+ */
219
+ sanitize(request) {
220
+ if (typeof request === 'string') {
221
+ return this.sanitizeString(request);
222
+ }
223
+
224
+ const sanitized = { ...request };
225
+
226
+ // Sanitize common fields
227
+ const fieldsToSanitize = ['prompt', 'input', 'text', 'content', 'message', 'query'];
228
+ for (const field of fieldsToSanitize) {
229
+ if (sanitized[field] && typeof sanitized[field] === 'string') {
230
+ sanitized[field] = this.sanitizeString(sanitized[field]);
231
+ }
232
+ }
233
+
234
+ // Handle messages array
235
+ if (Array.isArray(sanitized.messages)) {
236
+ sanitized.messages = sanitized.messages.map(msg => ({
237
+ ...msg,
238
+ content: msg.content ? this.sanitizeString(msg.content) : msg.content,
239
+ }));
240
+ }
241
+
242
+ return sanitized;
243
+ }
244
+
245
+ /**
246
+ * Sanitize a string
247
+ */
248
+ sanitizeString(str) {
249
+ let result = str;
250
+
251
+ // Remove null bytes
252
+ result = result.replace(/\0/g, '');
253
+
254
+ // Normalize unicode
255
+ result = result.normalize('NFC');
256
+
257
+ // Remove control characters (except newlines and tabs)
258
+ result = result.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
259
+
260
+ return result;
261
+ }
262
+
263
+ /**
264
+ * Update configuration
265
+ */
266
+ updateConfig(config) {
267
+ this.config = { ...this.config, ...config };
268
+ if (config.blockedKeywords) {
269
+ this.blockedKeywords = config.blockedKeywords;
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Add a custom injection pattern
275
+ */
276
+ addInjectionPattern(pattern) {
277
+ if (typeof pattern === 'string') {
278
+ pattern = new RegExp(pattern, 'i');
279
+ }
280
+ this.injectionPatterns.push(pattern);
281
+ }
282
+
283
+ /**
284
+ * Add a custom PII pattern
285
+ */
286
+ addPIIPattern(name, pattern) {
287
+ this.piiPatterns.push({ name, pattern });
288
+ }
289
+ }
290
+
291
+ export default InputValidator;