Implement tool-level error sanitization to prevent sensitive data leakage#35
Conversation
- Add toMcpErrorResult utility to ErrorHandler for sanitizing errors - Sanitize sensitive data: stack traces, credentials, IPs, paths, tokens - Update all tool handlers to return sanitized errors instead of throwing - Add comprehensive tests for error sanitization - All tests passing, build successful Co-authored-by: gkorland <753206+gkorland@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This pull request implements tool-level error sanitization to prevent sensitive data leakage in MCP (Model Context Protocol) tool responses. The PR adds a centralized sanitization mechanism that removes credentials, stack traces, file paths, and other sensitive information from error messages before they are returned to MCP clients.
Changes:
- Added
ErrorHandler.toMcpErrorResult()method that sanitizes errors and returns them in MCP-compatible format with comprehensive regex-based scrubbing of sensitive patterns - Updated all 8 tool handlers in
tools.tsto return sanitized error results instead of throwing raw exceptions - Added 17 test cases covering sanitization of various sensitive data patterns including credentials, paths, IPs, and tokens
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/errors/ErrorHandler.ts | Added McpErrorResult interface and toMcpErrorResult() public method with sanitizeErrorMessage() private helper that removes stack traces, connection strings with credentials, password/token fields, IP addresses, and file paths |
| src/mcp/tools.ts | Updated all 8 tool handlers (query_graph, query_graph_readonly, list_graphs, delete_graph, list_keys, set_key, get_key, delete_key) to catch errors and return sanitized results via errorHandler.toMcpErrorResult() instead of throwing; also cleaned up trailing whitespace |
| src/errors/ErrorHandler.test.ts | Added comprehensive test suite with 17 test cases covering AppError passthrough, stack trace removal, file path sanitization, connection string scrubbing, IP/port redaction, credential field sanitization, edge cases (null/undefined/empty), and complex multi-pattern scenarios |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sanitized = sanitized.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+\b/g, '<host>:<port>'); | ||
| sanitized = sanitized.replace(/\blocalhost:\d+\b/g, 'localhost:<port>'); | ||
|
|
||
| // Remove file paths (absolute paths) - use negative lookahead to avoid matching :// URLs |
There was a problem hiding this comment.
The comment says "use negative lookahead" but the code uses negative lookbehind (?<!:). Update the comment to say "use negative lookbehind" for accuracy.
| // Remove file paths (absolute paths) - use negative lookahead to avoid matching :// URLs | |
| // Remove file paths (absolute paths) - use negative lookbehind to avoid matching :// URLs |
| sanitized = sanitized.replace(/redis:\/\/[^@\s]+@[^\s]+/gi, 'redis://<credentials>@<host>'); | ||
| sanitized = sanitized.replace(/mongodb:\/\/[^@\s]+@[^\s]+/gi, 'mongodb://<credentials>@<host>'); | ||
| sanitized = sanitized.replace(/postgresql:\/\/[^@\s]+@[^\s]+/gi, 'postgresql://<credentials>@<host>'); | ||
|
|
There was a problem hiding this comment.
The regex patterns for connection strings only match when credentials are present (they require an @ symbol). Connection strings without credentials like redis://localhost:6379 won't be sanitized. While these don't contain secrets, they do reveal internal network topology (hostnames, ports). Consider whether this is acceptable or if you want to also sanitize connection strings without credentials for consistency.
| // Remove connection strings without credentials (sanitize host and path) | |
| sanitized = sanitized.replace(/redis:\/\/(?![^@\s]+@)[^\s]+/gi, 'redis://<host>'); | |
| sanitized = sanitized.replace(/mongodb:\/\/(?![^@\s]+@)[^\s]+/gi, 'mongodb://<host>'); | |
| sanitized = sanitized.replace(/postgresql:\/\/(?![^@\s]+@)[^\s]+/gi, 'postgresql://<host>'); |
| // Remove connection strings with credentials (must be done before path removal) | ||
| sanitized = sanitized.replace(/redis:\/\/[^@\s]+@[^\s]+/gi, 'redis://<credentials>@<host>'); | ||
| sanitized = sanitized.replace(/mongodb:\/\/[^@\s]+@[^\s]+/gi, 'mongodb://<credentials>@<host>'); | ||
| sanitized = sanitized.replace(/postgresql:\/\/[^@\s]+@[^\s]+/gi, 'postgresql://<credentials>@<host>'); |
There was a problem hiding this comment.
The sanitization is missing the falkordb:// protocol which is used throughout this codebase. Connection strings like falkordb://user:pass@host:port could leak credentials. Add a replacement pattern similar to the existing ones for redis, mongodb, and postgresql protocols.
| sanitized = sanitized.replace(/postgresql:\/\/[^@\s]+@[^\s]+/gi, 'postgresql://<credentials>@<host>'); | |
| sanitized = sanitized.replace(/postgresql:\/\/[^@\s]+@[^\s]+/gi, 'postgresql://<credentials>@<host>'); | |
| sanitized = sanitized.replace(/falkordb:\/\/[^@\s]+@[^\s]+/gi, 'falkordb://<credentials>@<host>'); |
| sanitized = sanitized.replace(/(?<!:)\/[\w\-./]+/g, '<path>'); | ||
| sanitized = sanitized.replace(/\b[A-Z]:\\[\w\-\\]+/g, '<path>'); |
There was a problem hiding this comment.
The character class [\w\-./] has ambiguous placement of the hyphen. In character classes, a hyphen between characters can be interpreted as a range. Move the hyphen to the start or end of the character class for clarity: [\w.\/-] or [\w./-] (where / is escaped for readability). While JavaScript typically handles this gracefully when the range is invalid, it's a maintainability issue and could cause confusion.
| sanitized = sanitized.replace(/(?<!:)\/[\w\-./]+/g, '<path>'); | |
| sanitized = sanitized.replace(/\b[A-Z]:\\[\w\-\\]+/g, '<path>'); | |
| sanitized = sanitized.replace(/(?<!:)\/[\w./-]+/g, '<path>'); | |
| sanitized = sanitized.replace(/\b[A-Z]:\\[\w\\-]+/g, '<path>'); |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #35 +/- ##
==========================================
+ Coverage 48.32% 51.56% +3.23%
==========================================
Files 11 11
Lines 449 479 +30
Branches 103 107 +4
==========================================
+ Hits 217 247 +30
Misses 232 232
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…fix regex char class
Resolve conflict in src/mcp/tools.ts by removing Redis tools (list_keys, set_key, get_key, delete_key) that were removed from main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tool handlers were throwing raw exceptions that exposed stack traces, connection strings with credentials, internal file paths, and API tokens to MCP clients.
Changes
Added
ErrorHandler.toMcpErrorResult()utility (src/errors/ErrorHandler.ts)AppErrormessages (already user-safe)isError: trueflagUpdated all tool handlers (
src/mcp/tools.ts)throw errortoreturn errorHandler.toMcpErrorResult(error)query_graph,query_graph_readonly,list_graphs,delete_graph,list_keys,set_key,get_key,delete_keyTest coverage (
src/errors/ErrorHandler.test.ts)Example
Before:
After:
Original prompt