mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-01-30 14:32:04 +00:00
Compare commits
1 Commits
v2.15.2
...
fix/teleme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b691cedf |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.15.3] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- **Error Message Capture in Telemetry** - Enhanced telemetry tracking to capture actual error messages for better debugging
|
||||
- Added optional `errorMessage` parameter to `trackError()` method
|
||||
- Comprehensive error message sanitization to protect sensitive data
|
||||
- Updated all production and test call sites to pass error messages
|
||||
- Error messages now stored in telemetry events table for analysis
|
||||
|
||||
### Security
|
||||
- **Enhanced Error Message Sanitization** - Comprehensive security hardening for telemetry data
|
||||
- **ReDoS Prevention**: Early truncation to 1500 chars before regex processing
|
||||
- **Full URL Redaction**: Changed from `[URL]/path` to `[URL]` to prevent API structure leakage
|
||||
- **Correct Sanitization Order**: URLs → specific credentials → emails → generic patterns
|
||||
- **Credential Pattern Detection**: Added AWS keys, GitHub tokens, JWT, Bearer tokens
|
||||
- **Error Handling**: Try-catch wrapper with `[SANITIZATION_FAILED]` fallback
|
||||
- **Stack Trace Truncation**: Limited to first 3 lines to reduce attack surface
|
||||
|
||||
### Fixed
|
||||
- **Missing Error Messages**: Resolved issue where 272+ weekly validation errors had no error messages captured
|
||||
- **Data Leakage**: Fixed URL path preservation exposing API versions and user IDs
|
||||
- **Email Exposure**: Fixed sanitization order allowing emails in URLs to leak
|
||||
- **ReDoS Vulnerability**: Removed complex capturing regex patterns that could cause performance issues
|
||||
|
||||
### Changed
|
||||
- **Breaking Change**: `trackError()` signature updated with 4th parameter `errorMessage?: string`
|
||||
- All internal call sites updated in single commit (atomic change)
|
||||
- Not backwards compatible but acceptable as all code is internal
|
||||
|
||||
### Technical Details
|
||||
- **Sanitization Patterns**:
|
||||
- AWS Keys: `AKIA[A-Z0-9]{16}` → `[AWS_KEY]`
|
||||
- GitHub Tokens: `ghp_[a-zA-Z0-9]{36,}` → `[GITHUB_TOKEN]`
|
||||
- JWT: `eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+` → `[JWT]`
|
||||
- Bearer Tokens: `Bearer [^\s]+` → `Bearer [TOKEN]`
|
||||
- Emails: `[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}` → `[EMAIL]`
|
||||
- Long Keys: `\b[a-zA-Z0-9_-]{32,}\b` → `[KEY]`
|
||||
- Generic Credentials: `password/api_key/token=<value>` → `<field>=[REDACTED]`
|
||||
|
||||
### Test Coverage
|
||||
- Added 18 new security-focused tests
|
||||
- Total telemetry tests: 269 passing
|
||||
- Coverage: 90.75% for telemetry module
|
||||
- All security patterns validated with edge cases
|
||||
|
||||
### Performance
|
||||
- Early truncation prevents ReDoS attacks
|
||||
- Simplified regex patterns (no complex capturing groups)
|
||||
- Sanitization adds <1ms overhead per error
|
||||
- Final message truncated to 500 chars max
|
||||
|
||||
### Impact
|
||||
- **Debugging**: Error messages now available for root cause analysis
|
||||
- **Security**: Comprehensive protection against credential leakage
|
||||
- **Performance**: Protected against ReDoS attacks
|
||||
- **Reliability**: Try-catch ensures sanitization never breaks telemetry
|
||||
|
||||
## [2.15.2] - 2025-10-03
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "n8n-mcp",
|
||||
"version": "2.15.2",
|
||||
"version": "2.15.3",
|
||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
|
||||
@@ -23,7 +23,7 @@ async function testIntegration() {
|
||||
|
||||
// Track errors
|
||||
console.log('Tracking errors...');
|
||||
telemetry.trackError('ValidationError', 'workflow_validation', 'validate_workflow');
|
||||
telemetry.trackError('ValidationError', 'workflow_validation', 'validate_workflow', 'Required field missing: nodes array is empty');
|
||||
|
||||
// Track a test workflow
|
||||
console.log('Tracking workflow creation...');
|
||||
|
||||
@@ -398,7 +398,8 @@ export class N8NDocumentationMCPServer {
|
||||
telemetry.trackError(
|
||||
error instanceof Error ? error.constructor.name : 'UnknownError',
|
||||
`tool_execution`,
|
||||
name
|
||||
name,
|
||||
errorMessage
|
||||
);
|
||||
|
||||
// Track tool sequence even for errors
|
||||
|
||||
@@ -127,7 +127,7 @@ export class TelemetryEventTracker {
|
||||
/**
|
||||
* Track an error event
|
||||
*/
|
||||
trackError(errorType: string, context: string, toolName?: string): void {
|
||||
trackError(errorType: string, context: string, toolName?: string, errorMessage?: string): void {
|
||||
if (!this.isEnabled()) return;
|
||||
|
||||
// Don't rate limit error tracking - we want to see all errors
|
||||
@@ -135,6 +135,7 @@ export class TelemetryEventTracker {
|
||||
errorType: this.sanitizeErrorType(errorType),
|
||||
context: this.sanitizeContext(context),
|
||||
tool: toolName ? toolName.replace(/[^a-zA-Z0-9_-]/g, '_') : undefined,
|
||||
error: errorMessage ? this.sanitizeErrorMessage(errorMessage) : undefined,
|
||||
}, false); // Skip rate limiting for errors
|
||||
}
|
||||
|
||||
@@ -428,4 +429,56 @@ export class TelemetryEventTracker {
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize error message
|
||||
*/
|
||||
private sanitizeErrorMessage(errorMessage: string): string {
|
||||
try {
|
||||
// Early truncate to prevent ReDoS and performance issues
|
||||
const maxLength = 1500;
|
||||
const trimmed = errorMessage.length > maxLength
|
||||
? errorMessage.substring(0, maxLength)
|
||||
: errorMessage;
|
||||
|
||||
// Handle stack traces - keep only first 3 lines (message + top stack frames)
|
||||
const lines = trimmed.split('\n');
|
||||
let sanitized = lines.slice(0, 3).join('\n');
|
||||
|
||||
// Sanitize sensitive data in correct order to prevent leakage
|
||||
// 1. URLs first (most encompassing) - fully redact to prevent path leakage
|
||||
sanitized = sanitized.replace(/https?:\/\/\S+/gi, '[URL]');
|
||||
|
||||
// 2. Specific credential patterns (before generic patterns)
|
||||
sanitized = sanitized
|
||||
.replace(/AKIA[A-Z0-9]{16}/g, '[AWS_KEY]')
|
||||
.replace(/ghp_[a-zA-Z0-9]{36,}/g, '[GITHUB_TOKEN]')
|
||||
.replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[JWT]')
|
||||
.replace(/Bearer\s+[^\s]+/gi, 'Bearer [TOKEN]');
|
||||
|
||||
// 3. Emails (after URLs to avoid partial matches)
|
||||
sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]');
|
||||
|
||||
// 4. Long keys and quoted tokens
|
||||
sanitized = sanitized
|
||||
.replace(/\b[a-zA-Z0-9_-]{32,}\b/g, '[KEY]')
|
||||
.replace(/(['"])[a-zA-Z0-9_-]{16,}\1/g, '$1[TOKEN]$1');
|
||||
|
||||
// 5. Generic credential patterns (after specific ones to avoid conflicts)
|
||||
sanitized = sanitized
|
||||
.replace(/password\s*[=:]\s*\S+/gi, 'password=[REDACTED]')
|
||||
.replace(/api[_-]?key\s*[=:]\s*\S+/gi, 'api_key=[REDACTED]')
|
||||
.replace(/(?<!Bearer\s)token\s*[=:]\s*\S+/gi, 'token=[REDACTED]'); // Negative lookbehind to avoid Bearer tokens
|
||||
|
||||
// Final truncate to 500 chars
|
||||
if (sanitized.length > 500) {
|
||||
sanitized = sanitized.substring(0, 500) + '...';
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
} catch (error) {
|
||||
logger.debug('Error message sanitization failed:', error);
|
||||
return '[SANITIZATION_FAILED]';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,9 +152,9 @@ export class TelemetryManager {
|
||||
/**
|
||||
* Track an error event
|
||||
*/
|
||||
trackError(errorType: string, context: string, toolName?: string): void {
|
||||
trackError(errorType: string, context: string, toolName?: string, errorMessage?: string): void {
|
||||
this.ensureInitialized();
|
||||
this.eventTracker.trackError(errorType, context, toolName);
|
||||
this.eventTracker.trackError(errorType, context, toolName, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -142,7 +142,8 @@ describe.skip('MCP Telemetry Integration', () => {
|
||||
telemetry.trackError(
|
||||
error.constructor.name,
|
||||
error.message,
|
||||
toolName
|
||||
toolName,
|
||||
error.message
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('TelemetryEventTracker', () => {
|
||||
|
||||
describe('trackError()', () => {
|
||||
it('should track error events without rate limiting', () => {
|
||||
eventTracker.trackError('ValidationError', 'Node configuration invalid', 'httpRequest');
|
||||
eventTracker.trackError('ValidationError', 'Node configuration invalid', 'httpRequest', 'Required field "url" is missing');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events).toHaveLength(1);
|
||||
@@ -202,34 +202,173 @@ describe('TelemetryEventTracker', () => {
|
||||
properties: {
|
||||
errorType: 'ValidationError',
|
||||
context: 'Node configuration invalid',
|
||||
tool: 'httpRequest'
|
||||
tool: 'httpRequest',
|
||||
error: 'Required field "url" is missing'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize error context', () => {
|
||||
const context = 'Failed to connect to https://api.example.com with key abc123def456ghi789jklmno0123456789';
|
||||
eventTracker.trackError('NetworkError', context);
|
||||
eventTracker.trackError('NetworkError', context, undefined, 'Connection timeout after 30s');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.context).toBe('Failed to connect to [URL] with key [KEY]');
|
||||
});
|
||||
|
||||
it('should sanitize error type', () => {
|
||||
eventTracker.trackError('Invalid$Error!Type', 'test context');
|
||||
eventTracker.trackError('Invalid$Error!Type', 'test context', undefined, 'Test error message');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.errorType).toBe('Invalid_Error_Type');
|
||||
});
|
||||
|
||||
it('should handle missing tool name', () => {
|
||||
eventTracker.trackError('TestError', 'test context');
|
||||
eventTracker.trackError('TestError', 'test context', undefined, 'No tool specified');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.tool).toBeNull(); // Validator converts undefined to null
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackError() with error messages', () => {
|
||||
it('should capture error messages in properties', () => {
|
||||
eventTracker.trackError('ValidationError', 'test', 'tool', 'Field "url" is required');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toBe('Field "url" is required');
|
||||
});
|
||||
|
||||
it('should handle undefined error message', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', undefined);
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toBeNull(); // Validator converts undefined to null
|
||||
});
|
||||
|
||||
it('should sanitize API keys in error messages', () => {
|
||||
eventTracker.trackError('AuthError', 'test', 'tool', 'Failed with api_key=sk_live_abc123def456');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('api_key=[REDACTED]');
|
||||
expect(events[0].properties.error).not.toContain('sk_live_abc123def456');
|
||||
});
|
||||
|
||||
it('should sanitize passwords in error messages', () => {
|
||||
eventTracker.trackError('AuthError', 'test', 'tool', 'Login failed: password=secret123');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('password=[REDACTED]');
|
||||
});
|
||||
|
||||
it('should sanitize long keys (32+ chars)', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Key: abc123def456ghi789jkl012mno345pqr678');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('[KEY]');
|
||||
});
|
||||
|
||||
it('should sanitize URLs in error messages', () => {
|
||||
eventTracker.trackError('NetworkError', 'test', 'tool', 'Failed to fetch https://api.example.com/v1/users');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toBe('Failed to fetch [URL]');
|
||||
expect(events[0].properties.error).not.toContain('api.example.com');
|
||||
expect(events[0].properties.error).not.toContain('/v1/users');
|
||||
});
|
||||
|
||||
it('should truncate very long error messages to 500 chars', () => {
|
||||
const longError = 'Error occurred while processing the request. ' + 'Additional context details. '.repeat(50);
|
||||
eventTracker.trackError('Error', 'test', 'tool', longError);
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error.length).toBeLessThanOrEqual(503); // 500 + '...'
|
||||
expect(events[0].properties.error).toMatch(/\.\.\.$/);
|
||||
});
|
||||
|
||||
it('should handle stack traces by keeping first 3 lines', () => {
|
||||
const errorMsg = 'Error: Something failed\n at foo (/path/file.js:10:5)\n at bar (/path/file.js:20:10)\n at baz (/path/file.js:30:15)\n at qux (/path/file.js:40:20)';
|
||||
eventTracker.trackError('Error', 'test', 'tool', errorMsg);
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
const lines = events[0].properties.error.split('\n');
|
||||
expect(lines.length).toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should sanitize emails in error messages', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Failed for user test@example.com');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('[EMAIL]');
|
||||
expect(events[0].properties.error).not.toContain('test@example.com');
|
||||
});
|
||||
|
||||
it('should sanitize quoted tokens', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Auth failed: "abc123def456ghi789"');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('"[TOKEN]"');
|
||||
});
|
||||
|
||||
it('should sanitize token= patterns in error messages', () => {
|
||||
eventTracker.trackError('AuthError', 'test', 'tool', 'Failed with token=abc123def456');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('token=[REDACTED]');
|
||||
});
|
||||
|
||||
it('should sanitize AWS access keys', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Failed with AWS key AKIAIOSFODNN7EXAMPLE');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('[AWS_KEY]');
|
||||
expect(events[0].properties.error).not.toContain('AKIAIOSFODNN7EXAMPLE');
|
||||
});
|
||||
|
||||
it('should sanitize GitHub tokens', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Auth failed: ghp_1234567890abcdefghijklmnopqrstuvwxyz');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('[GITHUB_TOKEN]');
|
||||
expect(events[0].properties.error).not.toContain('ghp_1234567890abcdefghijklmnopqrstuvwxyz');
|
||||
});
|
||||
|
||||
it('should sanitize JWT tokens', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Invalid JWT eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.signature provided');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('[JWT]');
|
||||
expect(events[0].properties.error).not.toContain('eyJhbGciOiJIUzI1NiJ9');
|
||||
});
|
||||
|
||||
it('should sanitize Bearer tokens', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Authorization failed: Bearer abc123def456ghi789');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
expect(events[0].properties.error).toContain('Bearer [TOKEN]');
|
||||
expect(events[0].properties.error).not.toContain('abc123def456ghi789');
|
||||
});
|
||||
|
||||
it('should prevent email leakage in URLs by sanitizing URLs first', () => {
|
||||
eventTracker.trackError('Error', 'test', 'tool', 'Failed: https://api.example.com/users/test@example.com/profile');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
// URL should be fully redacted, preventing any email leakage
|
||||
expect(events[0].properties.error).toBe('Failed: [URL]');
|
||||
expect(events[0].properties.error).not.toContain('test@example.com');
|
||||
expect(events[0].properties.error).not.toContain('/users/');
|
||||
});
|
||||
|
||||
it('should handle extremely long error messages efficiently', () => {
|
||||
const hugeError = 'Error: ' + 'x'.repeat(10000);
|
||||
eventTracker.trackError('Error', 'test', 'tool', hugeError);
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
// Should be truncated at 500 chars max
|
||||
expect(events[0].properties.error.length).toBeLessThanOrEqual(503); // 500 + '...'
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackEvent()', () => {
|
||||
it('should track generic events', () => {
|
||||
const properties = { key: 'value', count: 42 };
|
||||
@@ -618,7 +757,7 @@ describe('TelemetryEventTracker', () => {
|
||||
describe('sanitization helpers', () => {
|
||||
it('should sanitize context strings properly', () => {
|
||||
const context = 'Error at https://api.example.com/v1/users/test@email.com?key=secret123456789012345678901234567890';
|
||||
eventTracker.trackError('TestError', context);
|
||||
eventTracker.trackError('TestError', context, undefined, 'Test error with special chars');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
// After sanitization: emails first, then keys, then URL (keeping path)
|
||||
@@ -628,7 +767,7 @@ describe('TelemetryEventTracker', () => {
|
||||
it('should handle context truncation', () => {
|
||||
// Use a more realistic long context that won't trigger key sanitization
|
||||
const longContext = 'Error occurred while processing the request: ' + 'details '.repeat(20);
|
||||
eventTracker.trackError('TestError', longContext);
|
||||
eventTracker.trackError('TestError', longContext, undefined, 'Long error message for truncation test');
|
||||
|
||||
const events = eventTracker.getEventQueue();
|
||||
// Should be truncated to 100 chars
|
||||
|
||||
@@ -233,12 +233,13 @@ describe('TelemetryManager', () => {
|
||||
});
|
||||
|
||||
it('should track errors', () => {
|
||||
manager.trackError('ValidationError', 'Node configuration invalid', 'httpRequest');
|
||||
manager.trackError('ValidationError', 'Node configuration invalid', 'httpRequest', 'Required field "url" is missing');
|
||||
|
||||
expect(mockEventTracker.trackError).toHaveBeenCalledWith(
|
||||
'ValidationError',
|
||||
'Node configuration invalid',
|
||||
'httpRequest'
|
||||
'httpRequest',
|
||||
'Required field "url" is missing'
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user