ntfy-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.
- package/LICENSE +201 -0
- package/README.md +423 -0
- package/dist/config/index.d.ts +23 -0
- package/dist/config/index.js +111 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +108 -0
- package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.d.ts +2 -0
- package/dist/mcp-server/resources/ntfyResource/getNtfyTopic.js +111 -0
- package/dist/mcp-server/resources/ntfyResource/index.d.ts +12 -0
- package/dist/mcp-server/resources/ntfyResource/index.js +72 -0
- package/dist/mcp-server/resources/ntfyResource/types.d.ts +27 -0
- package/dist/mcp-server/resources/ntfyResource/types.js +8 -0
- package/dist/mcp-server/server.d.ts +40 -0
- package/dist/mcp-server/server.js +245 -0
- package/dist/mcp-server/tools/ntfyTool/index.d.ts +11 -0
- package/dist/mcp-server/tools/ntfyTool/index.js +110 -0
- package/dist/mcp-server/tools/ntfyTool/ntfyMessage.d.ts +9 -0
- package/dist/mcp-server/tools/ntfyTool/ntfyMessage.js +289 -0
- package/dist/mcp-server/tools/ntfyTool/types.d.ts +252 -0
- package/dist/mcp-server/tools/ntfyTool/types.js +144 -0
- package/dist/mcp-server/utils/registrationHelper.d.ts +48 -0
- package/dist/mcp-server/utils/registrationHelper.js +63 -0
- package/dist/services/ntfy/constants.d.ts +37 -0
- package/dist/services/ntfy/constants.js +37 -0
- package/dist/services/ntfy/errors.d.ts +79 -0
- package/dist/services/ntfy/errors.js +134 -0
- package/dist/services/ntfy/index.d.ts +33 -0
- package/dist/services/ntfy/index.js +56 -0
- package/dist/services/ntfy/publisher.d.ts +66 -0
- package/dist/services/ntfy/publisher.js +229 -0
- package/dist/services/ntfy/subscriber.d.ts +81 -0
- package/dist/services/ntfy/subscriber.js +502 -0
- package/dist/services/ntfy/types.d.ts +161 -0
- package/dist/services/ntfy/types.js +4 -0
- package/dist/services/ntfy/utils.d.ts +85 -0
- package/dist/services/ntfy/utils.js +410 -0
- package/dist/types-global/errors.d.ts +35 -0
- package/dist/types-global/errors.js +39 -0
- package/dist/types-global/mcp.d.ts +30 -0
- package/dist/types-global/mcp.js +25 -0
- package/dist/types-global/tool.d.ts +61 -0
- package/dist/types-global/tool.js +99 -0
- package/dist/utils/errorHandler.d.ts +98 -0
- package/dist/utils/errorHandler.js +271 -0
- package/dist/utils/idGenerator.d.ts +94 -0
- package/dist/utils/idGenerator.js +149 -0
- package/dist/utils/index.d.ts +13 -0
- package/dist/utils/index.js +16 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.js +92 -0
- package/dist/utils/rateLimiter.d.ts +115 -0
- package/dist/utils/rateLimiter.js +180 -0
- package/dist/utils/requestContext.d.ts +68 -0
- package/dist/utils/requestContext.js +91 -0
- package/dist/utils/sanitization.d.ts +224 -0
- package/dist/utils/sanitization.js +367 -0
- package/dist/utils/security.d.ts +26 -0
- package/dist/utils/security.js +27 -0
- package/package.json +47 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Request context utilities class
|
|
4
|
+
*/
|
|
5
|
+
export class RequestContextService {
|
|
6
|
+
/**
|
|
7
|
+
* Private constructor to enforce singleton pattern
|
|
8
|
+
*/
|
|
9
|
+
constructor() {
|
|
10
|
+
this.config = {};
|
|
11
|
+
logger.debug('RequestContext service initialized');
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get the singleton RequestContextService instance
|
|
15
|
+
* @returns RequestContextService instance
|
|
16
|
+
*/
|
|
17
|
+
static getInstance() {
|
|
18
|
+
if (!RequestContextService.instance) {
|
|
19
|
+
RequestContextService.instance = new RequestContextService();
|
|
20
|
+
}
|
|
21
|
+
return RequestContextService.instance;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Configure service settings
|
|
25
|
+
* @param config New configuration
|
|
26
|
+
* @returns Updated configuration
|
|
27
|
+
*/
|
|
28
|
+
configure(config) {
|
|
29
|
+
this.config = {
|
|
30
|
+
...this.config,
|
|
31
|
+
...config
|
|
32
|
+
};
|
|
33
|
+
logger.debug('RequestContext configuration updated');
|
|
34
|
+
return { ...this.config };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get current configuration
|
|
38
|
+
* @returns Current configuration
|
|
39
|
+
*/
|
|
40
|
+
getConfig() {
|
|
41
|
+
return { ...this.config };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Create a request context with unique ID and timestamp
|
|
45
|
+
* @param additionalContext Additional context properties
|
|
46
|
+
* @returns Request context object
|
|
47
|
+
*/
|
|
48
|
+
createRequestContext(additionalContext = {}) {
|
|
49
|
+
const requestId = crypto.randomUUID();
|
|
50
|
+
const timestamp = new Date().toISOString();
|
|
51
|
+
return {
|
|
52
|
+
requestId,
|
|
53
|
+
timestamp,
|
|
54
|
+
...additionalContext
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Generate a secure random string
|
|
59
|
+
* @param length Length of the string
|
|
60
|
+
* @param chars Character set to use
|
|
61
|
+
* @returns Random string
|
|
62
|
+
*/
|
|
63
|
+
generateSecureRandomString(length = 32, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
|
|
64
|
+
const randomValues = new Uint8Array(length);
|
|
65
|
+
crypto.getRandomValues(randomValues);
|
|
66
|
+
let result = '';
|
|
67
|
+
for (let i = 0; i < length; i++) {
|
|
68
|
+
result += chars[randomValues[i] % chars.length];
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Create and export singleton instance
|
|
74
|
+
export const requestContextService = RequestContextService.getInstance();
|
|
75
|
+
// Export convenience functions that delegate to the singleton instance
|
|
76
|
+
export const configureContext = (config) => {
|
|
77
|
+
return requestContextService.configure(config);
|
|
78
|
+
};
|
|
79
|
+
export const createRequestContext = (additionalContext = {}) => {
|
|
80
|
+
return requestContextService.createRequestContext(additionalContext);
|
|
81
|
+
};
|
|
82
|
+
export const generateSecureRandomString = (length = 32, chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') => {
|
|
83
|
+
return requestContextService.generateSecureRandomString(length, chars);
|
|
84
|
+
};
|
|
85
|
+
// Export default utilities
|
|
86
|
+
export default {
|
|
87
|
+
requestContextService,
|
|
88
|
+
configureContext,
|
|
89
|
+
createRequestContext,
|
|
90
|
+
generateSecureRandomString
|
|
91
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html';
|
|
2
|
+
/**
|
|
3
|
+
* Options for path sanitization
|
|
4
|
+
*/
|
|
5
|
+
export interface PathSanitizeOptions {
|
|
6
|
+
/** Restrict paths to a specific root directory */
|
|
7
|
+
rootDir?: string;
|
|
8
|
+
/** Normalize Windows-style paths to POSIX-style */
|
|
9
|
+
toPosix?: boolean;
|
|
10
|
+
/** Allow absolute paths (if false, converts to relative paths) */
|
|
11
|
+
allowAbsolute?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Context-specific input sanitization options
|
|
15
|
+
*/
|
|
16
|
+
export interface SanitizeStringOptions {
|
|
17
|
+
/** Handle content differently based on context */
|
|
18
|
+
context?: 'text' | 'html' | 'attribute' | 'url' | 'javascript';
|
|
19
|
+
/** Custom allowed tags when using html context */
|
|
20
|
+
allowedTags?: string[];
|
|
21
|
+
/** Custom allowed attributes when using html context */
|
|
22
|
+
allowedAttributes?: Record<string, string[]>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Configuration for HTML sanitization
|
|
26
|
+
*/
|
|
27
|
+
export interface HtmlSanitizeConfig {
|
|
28
|
+
/** Allowed HTML tags */
|
|
29
|
+
allowedTags?: string[];
|
|
30
|
+
/** Allowed HTML attributes (global or per-tag) */
|
|
31
|
+
allowedAttributes?: sanitizeHtml.IOptions['allowedAttributes'];
|
|
32
|
+
/** Allow preserving comments - uses allowedTags internally */
|
|
33
|
+
preserveComments?: boolean;
|
|
34
|
+
/** Custom URL sanitizer */
|
|
35
|
+
transformTags?: sanitizeHtml.IOptions['transformTags'];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Sanitization class for handling various input sanitization tasks
|
|
39
|
+
*/
|
|
40
|
+
export declare class Sanitization {
|
|
41
|
+
private static instance;
|
|
42
|
+
/** Default list of sensitive fields for sanitizing logs */
|
|
43
|
+
private sensitiveFields;
|
|
44
|
+
/** Default sanitize-html configuration */
|
|
45
|
+
private defaultHtmlSanitizeConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Private constructor to enforce singleton pattern
|
|
48
|
+
*/
|
|
49
|
+
private constructor();
|
|
50
|
+
/**
|
|
51
|
+
* Get the singleton Sanitization instance
|
|
52
|
+
* @returns Sanitization instance
|
|
53
|
+
*/
|
|
54
|
+
static getInstance(): Sanitization;
|
|
55
|
+
/**
|
|
56
|
+
* Set sensitive fields for log sanitization
|
|
57
|
+
* @param fields Array of field names to consider sensitive
|
|
58
|
+
*/
|
|
59
|
+
setSensitiveFields(fields: string[]): void;
|
|
60
|
+
/**
|
|
61
|
+
* Get the current list of sensitive fields
|
|
62
|
+
* @returns Array of sensitive field names
|
|
63
|
+
*/
|
|
64
|
+
getSensitiveFields(): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Sanitize HTML content using sanitize-html library
|
|
67
|
+
* @param input HTML string to sanitize
|
|
68
|
+
* @param config Optional custom sanitization config
|
|
69
|
+
* @returns Sanitized HTML
|
|
70
|
+
*/
|
|
71
|
+
sanitizeHtml(input: string, config?: HtmlSanitizeConfig): string;
|
|
72
|
+
/**
|
|
73
|
+
* Sanitize string input based on context
|
|
74
|
+
* @param input String to sanitize
|
|
75
|
+
* @param options Sanitization options
|
|
76
|
+
* @returns Sanitized string
|
|
77
|
+
*/
|
|
78
|
+
sanitizeString(input: string, options?: SanitizeStringOptions): string;
|
|
79
|
+
/**
|
|
80
|
+
* Sanitize URL with robust validation and sanitization
|
|
81
|
+
* @param input URL to sanitize
|
|
82
|
+
* @param allowedProtocols Allowed URL protocols
|
|
83
|
+
* @returns Sanitized URL
|
|
84
|
+
*/
|
|
85
|
+
sanitizeUrl(input: string, allowedProtocols?: string[]): string;
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize file paths to prevent path traversal attacks
|
|
88
|
+
* @param input Path to sanitize
|
|
89
|
+
* @param options Options for path sanitization
|
|
90
|
+
* @returns Sanitized and normalized path
|
|
91
|
+
*/
|
|
92
|
+
sanitizePath(input: string, options?: PathSanitizeOptions): string;
|
|
93
|
+
/**
|
|
94
|
+
* Sanitize a JSON string
|
|
95
|
+
* @param input JSON string to sanitize
|
|
96
|
+
* @param maxSize Maximum allowed size in bytes
|
|
97
|
+
* @returns Parsed and sanitized object
|
|
98
|
+
*/
|
|
99
|
+
sanitizeJson<T = unknown>(input: string, maxSize?: number): T;
|
|
100
|
+
/**
|
|
101
|
+
* Ensure input is within a numeric range
|
|
102
|
+
* @param input Number to validate
|
|
103
|
+
* @param min Minimum allowed value
|
|
104
|
+
* @param max Maximum allowed value
|
|
105
|
+
* @returns Sanitized number within range
|
|
106
|
+
*/
|
|
107
|
+
sanitizeNumber(input: number | string, min?: number, max?: number): number;
|
|
108
|
+
/**
|
|
109
|
+
* Sanitize input for logging to protect sensitive information
|
|
110
|
+
* @param input Input to sanitize
|
|
111
|
+
* @returns Sanitized input safe for logging
|
|
112
|
+
*/
|
|
113
|
+
sanitizeForLogging(input: unknown): unknown;
|
|
114
|
+
/**
|
|
115
|
+
* Private helper to convert attribute format from record to sanitize-html format
|
|
116
|
+
*/
|
|
117
|
+
private convertAttributesFormat;
|
|
118
|
+
/**
|
|
119
|
+
* Recursively redact sensitive fields in an object
|
|
120
|
+
*/
|
|
121
|
+
private redactSensitiveFields;
|
|
122
|
+
}
|
|
123
|
+
export declare const sanitization: Sanitization;
|
|
124
|
+
export declare const sanitizeInput: {
|
|
125
|
+
/**
|
|
126
|
+
* Remove potentially dangerous characters from strings based on context
|
|
127
|
+
* @param input String to sanitize
|
|
128
|
+
* @param options Sanitization options for context-specific handling
|
|
129
|
+
* @returns Sanitized string
|
|
130
|
+
*/
|
|
131
|
+
string: (input: string, options?: SanitizeStringOptions) => string;
|
|
132
|
+
/**
|
|
133
|
+
* Sanitize HTML to prevent XSS
|
|
134
|
+
* @param input HTML string to sanitize
|
|
135
|
+
* @param config Optional custom sanitization config
|
|
136
|
+
* @returns Sanitized HTML
|
|
137
|
+
*/
|
|
138
|
+
html: (input: string, config?: HtmlSanitizeConfig) => string;
|
|
139
|
+
/**
|
|
140
|
+
* Sanitize URLs
|
|
141
|
+
* @param input URL to sanitize
|
|
142
|
+
* @param allowedProtocols Allowed URL protocols
|
|
143
|
+
* @returns Sanitized URL
|
|
144
|
+
*/
|
|
145
|
+
url: (input: string, allowedProtocols?: string[]) => string;
|
|
146
|
+
/**
|
|
147
|
+
* Sanitize file paths to prevent path traversal attacks
|
|
148
|
+
* @param input Path to sanitize
|
|
149
|
+
* @param options Options for path sanitization
|
|
150
|
+
* @returns Sanitized and normalized path
|
|
151
|
+
*/
|
|
152
|
+
path: (input: string, options?: PathSanitizeOptions) => string;
|
|
153
|
+
/**
|
|
154
|
+
* Sanitize a JSON string
|
|
155
|
+
* @param input JSON string to sanitize
|
|
156
|
+
* @param maxSize Maximum allowed size in bytes
|
|
157
|
+
* @returns Parsed and sanitized object
|
|
158
|
+
*/
|
|
159
|
+
json: <T = unknown>(input: string, maxSize?: number) => T;
|
|
160
|
+
/**
|
|
161
|
+
* Ensure input is within a numeric range
|
|
162
|
+
* @param input Number to validate
|
|
163
|
+
* @param min Minimum allowed value
|
|
164
|
+
* @param max Maximum allowed value
|
|
165
|
+
* @returns Sanitized number within range
|
|
166
|
+
*/
|
|
167
|
+
number: (input: number | string, min?: number, max?: number) => number;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Sanitize input for logging to protect sensitive information
|
|
171
|
+
* @param input Input to sanitize
|
|
172
|
+
* @returns Sanitized input safe for logging
|
|
173
|
+
*/
|
|
174
|
+
export declare const sanitizeInputForLogging: (input: unknown) => unknown;
|
|
175
|
+
declare const _default: {
|
|
176
|
+
sanitization: Sanitization;
|
|
177
|
+
sanitizeInput: {
|
|
178
|
+
/**
|
|
179
|
+
* Remove potentially dangerous characters from strings based on context
|
|
180
|
+
* @param input String to sanitize
|
|
181
|
+
* @param options Sanitization options for context-specific handling
|
|
182
|
+
* @returns Sanitized string
|
|
183
|
+
*/
|
|
184
|
+
string: (input: string, options?: SanitizeStringOptions) => string;
|
|
185
|
+
/**
|
|
186
|
+
* Sanitize HTML to prevent XSS
|
|
187
|
+
* @param input HTML string to sanitize
|
|
188
|
+
* @param config Optional custom sanitization config
|
|
189
|
+
* @returns Sanitized HTML
|
|
190
|
+
*/
|
|
191
|
+
html: (input: string, config?: HtmlSanitizeConfig) => string;
|
|
192
|
+
/**
|
|
193
|
+
* Sanitize URLs
|
|
194
|
+
* @param input URL to sanitize
|
|
195
|
+
* @param allowedProtocols Allowed URL protocols
|
|
196
|
+
* @returns Sanitized URL
|
|
197
|
+
*/
|
|
198
|
+
url: (input: string, allowedProtocols?: string[]) => string;
|
|
199
|
+
/**
|
|
200
|
+
* Sanitize file paths to prevent path traversal attacks
|
|
201
|
+
* @param input Path to sanitize
|
|
202
|
+
* @param options Options for path sanitization
|
|
203
|
+
* @returns Sanitized and normalized path
|
|
204
|
+
*/
|
|
205
|
+
path: (input: string, options?: PathSanitizeOptions) => string;
|
|
206
|
+
/**
|
|
207
|
+
* Sanitize a JSON string
|
|
208
|
+
* @param input JSON string to sanitize
|
|
209
|
+
* @param maxSize Maximum allowed size in bytes
|
|
210
|
+
* @returns Parsed and sanitized object
|
|
211
|
+
*/
|
|
212
|
+
json: <T = unknown>(input: string, maxSize?: number) => T;
|
|
213
|
+
/**
|
|
214
|
+
* Ensure input is within a numeric range
|
|
215
|
+
* @param input Number to validate
|
|
216
|
+
* @param min Minimum allowed value
|
|
217
|
+
* @param max Maximum allowed value
|
|
218
|
+
* @returns Sanitized number within range
|
|
219
|
+
*/
|
|
220
|
+
number: (input: number | string, min?: number, max?: number) => number;
|
|
221
|
+
};
|
|
222
|
+
sanitizeInputForLogging: (input: unknown) => unknown;
|
|
223
|
+
};
|
|
224
|
+
export default _default;
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import normalize from 'path-normalize';
|
|
3
|
+
import sanitizeHtml from 'sanitize-html';
|
|
4
|
+
import validator from 'validator';
|
|
5
|
+
import * as xssFilters from 'xss-filters';
|
|
6
|
+
import { BaseErrorCode, McpError } from '../types-global/errors.js';
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Sanitization class for handling various input sanitization tasks
|
|
10
|
+
*/
|
|
11
|
+
export class Sanitization {
|
|
12
|
+
/**
|
|
13
|
+
* Private constructor to enforce singleton pattern
|
|
14
|
+
*/
|
|
15
|
+
constructor() {
|
|
16
|
+
/** Default list of sensitive fields for sanitizing logs */
|
|
17
|
+
this.sensitiveFields = [
|
|
18
|
+
'password', 'token', 'secret', 'key', 'apiKey', 'auth',
|
|
19
|
+
'credential', 'jwt', 'ssn', 'credit', 'card', 'cvv', 'authorization'
|
|
20
|
+
];
|
|
21
|
+
/** Default sanitize-html configuration */
|
|
22
|
+
this.defaultHtmlSanitizeConfig = {
|
|
23
|
+
allowedTags: [
|
|
24
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol',
|
|
25
|
+
'li', 'b', 'i', 'strong', 'em', 'strike', 'code', 'hr', 'br',
|
|
26
|
+
'div', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'pre'
|
|
27
|
+
],
|
|
28
|
+
allowedAttributes: {
|
|
29
|
+
'a': ['href', 'name', 'target'],
|
|
30
|
+
'img': ['src', 'alt', 'title', 'width', 'height'],
|
|
31
|
+
'*': ['class', 'id', 'style']
|
|
32
|
+
},
|
|
33
|
+
preserveComments: false
|
|
34
|
+
};
|
|
35
|
+
logger.debug('Sanitization service initialized with modern libraries');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the singleton Sanitization instance
|
|
39
|
+
* @returns Sanitization instance
|
|
40
|
+
*/
|
|
41
|
+
static getInstance() {
|
|
42
|
+
if (!Sanitization.instance) {
|
|
43
|
+
Sanitization.instance = new Sanitization();
|
|
44
|
+
}
|
|
45
|
+
return Sanitization.instance;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Set sensitive fields for log sanitization
|
|
49
|
+
* @param fields Array of field names to consider sensitive
|
|
50
|
+
*/
|
|
51
|
+
setSensitiveFields(fields) {
|
|
52
|
+
this.sensitiveFields = [...this.sensitiveFields, ...fields];
|
|
53
|
+
logger.debug('Updated sensitive fields list', { count: this.sensitiveFields.length });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get the current list of sensitive fields
|
|
57
|
+
* @returns Array of sensitive field names
|
|
58
|
+
*/
|
|
59
|
+
getSensitiveFields() {
|
|
60
|
+
return [...this.sensitiveFields];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sanitize HTML content using sanitize-html library
|
|
64
|
+
* @param input HTML string to sanitize
|
|
65
|
+
* @param config Optional custom sanitization config
|
|
66
|
+
* @returns Sanitized HTML
|
|
67
|
+
*/
|
|
68
|
+
sanitizeHtml(input, config) {
|
|
69
|
+
if (!input)
|
|
70
|
+
return '';
|
|
71
|
+
// Create sanitize-html options from our config
|
|
72
|
+
const options = {
|
|
73
|
+
allowedTags: config?.allowedTags || this.defaultHtmlSanitizeConfig.allowedTags,
|
|
74
|
+
allowedAttributes: config?.allowedAttributes || this.defaultHtmlSanitizeConfig.allowedAttributes,
|
|
75
|
+
transformTags: config?.transformTags
|
|
76
|
+
};
|
|
77
|
+
// Handle comments - if preserveComments is true, add '!--' to allowedTags
|
|
78
|
+
if (config?.preserveComments || this.defaultHtmlSanitizeConfig.preserveComments) {
|
|
79
|
+
options.allowedTags = [...(options.allowedTags || []), '!--'];
|
|
80
|
+
}
|
|
81
|
+
return sanitizeHtml(input, options);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sanitize string input based on context
|
|
85
|
+
* @param input String to sanitize
|
|
86
|
+
* @param options Sanitization options
|
|
87
|
+
* @returns Sanitized string
|
|
88
|
+
*/
|
|
89
|
+
sanitizeString(input, options = {}) {
|
|
90
|
+
if (!input)
|
|
91
|
+
return '';
|
|
92
|
+
// Handle based on context
|
|
93
|
+
switch (options.context) {
|
|
94
|
+
case 'html':
|
|
95
|
+
// Use sanitize-html with custom options
|
|
96
|
+
return this.sanitizeHtml(input, {
|
|
97
|
+
allowedTags: options.allowedTags,
|
|
98
|
+
allowedAttributes: options.allowedAttributes ?
|
|
99
|
+
this.convertAttributesFormat(options.allowedAttributes) :
|
|
100
|
+
undefined
|
|
101
|
+
});
|
|
102
|
+
case 'attribute':
|
|
103
|
+
// Use xss-filters for HTML attributes
|
|
104
|
+
return xssFilters.inHTMLData(input);
|
|
105
|
+
case 'url':
|
|
106
|
+
// Validate and sanitize URL
|
|
107
|
+
if (!validator.isURL(input, {
|
|
108
|
+
protocols: ['http', 'https'],
|
|
109
|
+
require_protocol: true
|
|
110
|
+
})) {
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
return validator.trim(input);
|
|
114
|
+
case 'javascript':
|
|
115
|
+
// Reject any attempt to sanitize JavaScript
|
|
116
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'JavaScript sanitization not supported through string sanitizer', { input: input.substring(0, 50) });
|
|
117
|
+
case 'text':
|
|
118
|
+
default:
|
|
119
|
+
// Use XSS filters for basic text
|
|
120
|
+
return xssFilters.inHTMLData(input);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Sanitize URL with robust validation and sanitization
|
|
125
|
+
* @param input URL to sanitize
|
|
126
|
+
* @param allowedProtocols Allowed URL protocols
|
|
127
|
+
* @returns Sanitized URL
|
|
128
|
+
*/
|
|
129
|
+
sanitizeUrl(input, allowedProtocols = ['http', 'https']) {
|
|
130
|
+
try {
|
|
131
|
+
// First validate the URL format
|
|
132
|
+
if (!validator.isURL(input, {
|
|
133
|
+
protocols: allowedProtocols,
|
|
134
|
+
require_protocol: true
|
|
135
|
+
})) {
|
|
136
|
+
throw new Error('Invalid URL format');
|
|
137
|
+
}
|
|
138
|
+
// Double-check no javascript: protocol sneaked in
|
|
139
|
+
if (input.toLowerCase().includes('javascript:')) {
|
|
140
|
+
throw new Error('JavaScript protocol not allowed');
|
|
141
|
+
}
|
|
142
|
+
// Sanitize and return
|
|
143
|
+
return validator.trim(xssFilters.uriInHTMLData(input));
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid URL format', { input, error: error instanceof Error ? error.message : String(error) });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sanitize file paths to prevent path traversal attacks
|
|
151
|
+
* @param input Path to sanitize
|
|
152
|
+
* @param options Options for path sanitization
|
|
153
|
+
* @returns Sanitized and normalized path
|
|
154
|
+
*/
|
|
155
|
+
sanitizePath(input, options = {}) {
|
|
156
|
+
try {
|
|
157
|
+
if (!input) {
|
|
158
|
+
throw new Error('Empty path');
|
|
159
|
+
}
|
|
160
|
+
// Apply path normalization (resolves '..' and '.' segments properly)
|
|
161
|
+
let normalized = normalize(input);
|
|
162
|
+
// Convert backslashes to forward slashes if toPosix is true
|
|
163
|
+
if (options.toPosix) {
|
|
164
|
+
normalized = normalized.replace(/\\/g, '/');
|
|
165
|
+
}
|
|
166
|
+
// Handle absolute paths based on allowAbsolute option
|
|
167
|
+
if (!options.allowAbsolute && path.isAbsolute(normalized)) {
|
|
168
|
+
// Remove leading slash or drive letter to make it relative
|
|
169
|
+
normalized = normalized.replace(/^(?:[A-Za-z]:)?[/\\]/, '');
|
|
170
|
+
}
|
|
171
|
+
// If rootDir is specified, ensure the path doesn't escape it
|
|
172
|
+
if (options.rootDir) {
|
|
173
|
+
const rootDir = path.resolve(options.rootDir);
|
|
174
|
+
// Resolve the normalized path against the root dir
|
|
175
|
+
const fullPath = path.resolve(rootDir, normalized);
|
|
176
|
+
// More robust check for path traversal
|
|
177
|
+
if (!fullPath.startsWith(rootDir + path.sep) && fullPath !== rootDir) {
|
|
178
|
+
throw new Error('Path traversal detected');
|
|
179
|
+
}
|
|
180
|
+
// Return the path relative to the root
|
|
181
|
+
return path.relative(rootDir, fullPath);
|
|
182
|
+
}
|
|
183
|
+
// Final validation - ensure the path doesn't contain suspicious patterns
|
|
184
|
+
if (normalized.includes('\0') || normalized.match(/\\\\[.?]|\.\.\\/)) {
|
|
185
|
+
throw new Error('Invalid path characters detected');
|
|
186
|
+
}
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Log the error for debugging
|
|
191
|
+
logger.warn('Path sanitization error', {
|
|
192
|
+
input,
|
|
193
|
+
error: error instanceof Error ? error.message : String(error)
|
|
194
|
+
});
|
|
195
|
+
// Return a safe default in case of errors
|
|
196
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid or unsafe path', { input });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Sanitize a JSON string
|
|
201
|
+
* @param input JSON string to sanitize
|
|
202
|
+
* @param maxSize Maximum allowed size in bytes
|
|
203
|
+
* @returns Parsed and sanitized object
|
|
204
|
+
*/
|
|
205
|
+
sanitizeJson(input, maxSize) {
|
|
206
|
+
try {
|
|
207
|
+
// Check size limit if specified
|
|
208
|
+
if (maxSize !== undefined && input.length > maxSize) {
|
|
209
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `JSON exceeds maximum allowed size of ${maxSize} bytes`, { size: input.length, maxSize });
|
|
210
|
+
}
|
|
211
|
+
// Validate JSON format
|
|
212
|
+
if (!validator.isJSON(input)) {
|
|
213
|
+
throw new Error('Invalid JSON format');
|
|
214
|
+
}
|
|
215
|
+
return JSON.parse(input);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof McpError) {
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid JSON format', { input: input.length > 100 ? `${input.substring(0, 100)}...` : input });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Ensure input is within a numeric range
|
|
226
|
+
* @param input Number to validate
|
|
227
|
+
* @param min Minimum allowed value
|
|
228
|
+
* @param max Maximum allowed value
|
|
229
|
+
* @returns Sanitized number within range
|
|
230
|
+
*/
|
|
231
|
+
sanitizeNumber(input, min, max) {
|
|
232
|
+
let value;
|
|
233
|
+
// Handle string input
|
|
234
|
+
if (typeof input === 'string') {
|
|
235
|
+
if (!validator.isNumeric(input)) {
|
|
236
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid number format', { input });
|
|
237
|
+
}
|
|
238
|
+
value = parseFloat(input);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
value = input;
|
|
242
|
+
}
|
|
243
|
+
if (isNaN(value)) {
|
|
244
|
+
throw new McpError(BaseErrorCode.VALIDATION_ERROR, 'Invalid number format', { input });
|
|
245
|
+
}
|
|
246
|
+
if (min !== undefined && value < min) {
|
|
247
|
+
value = min;
|
|
248
|
+
}
|
|
249
|
+
if (max !== undefined && value > max) {
|
|
250
|
+
value = max;
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Sanitize input for logging to protect sensitive information
|
|
256
|
+
* @param input Input to sanitize
|
|
257
|
+
* @returns Sanitized input safe for logging
|
|
258
|
+
*/
|
|
259
|
+
sanitizeForLogging(input) {
|
|
260
|
+
if (!input || typeof input !== 'object') {
|
|
261
|
+
return input;
|
|
262
|
+
}
|
|
263
|
+
// Create a deep copy to avoid modifying the original
|
|
264
|
+
const sanitized = Array.isArray(input)
|
|
265
|
+
? [...input]
|
|
266
|
+
: { ...input };
|
|
267
|
+
// Recursively sanitize the object
|
|
268
|
+
this.redactSensitiveFields(sanitized);
|
|
269
|
+
return sanitized;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Private helper to convert attribute format from record to sanitize-html format
|
|
273
|
+
*/
|
|
274
|
+
convertAttributesFormat(attrs) {
|
|
275
|
+
const result = {};
|
|
276
|
+
for (const [tag, attributes] of Object.entries(attrs)) {
|
|
277
|
+
result[tag] = attributes;
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Recursively redact sensitive fields in an object
|
|
283
|
+
*/
|
|
284
|
+
redactSensitiveFields(obj) {
|
|
285
|
+
if (!obj || typeof obj !== 'object') {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Handle arrays
|
|
289
|
+
if (Array.isArray(obj)) {
|
|
290
|
+
obj.forEach(item => this.redactSensitiveFields(item));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Handle regular objects
|
|
294
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
295
|
+
// Check if this key matches any sensitive field pattern
|
|
296
|
+
const isSensitive = this.sensitiveFields.some(field => key.toLowerCase().includes(field.toLowerCase()));
|
|
297
|
+
if (isSensitive) {
|
|
298
|
+
// Mask sensitive value
|
|
299
|
+
obj[key] = '[REDACTED]';
|
|
300
|
+
}
|
|
301
|
+
else if (value && typeof value === 'object') {
|
|
302
|
+
// Recursively process nested objects
|
|
303
|
+
this.redactSensitiveFields(value);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Create and export singleton instance
|
|
309
|
+
export const sanitization = Sanitization.getInstance();
|
|
310
|
+
// Export the input sanitization object with convenience functions
|
|
311
|
+
export const sanitizeInput = {
|
|
312
|
+
/**
|
|
313
|
+
* Remove potentially dangerous characters from strings based on context
|
|
314
|
+
* @param input String to sanitize
|
|
315
|
+
* @param options Sanitization options for context-specific handling
|
|
316
|
+
* @returns Sanitized string
|
|
317
|
+
*/
|
|
318
|
+
string: (input, options = {}) => sanitization.sanitizeString(input, options),
|
|
319
|
+
/**
|
|
320
|
+
* Sanitize HTML to prevent XSS
|
|
321
|
+
* @param input HTML string to sanitize
|
|
322
|
+
* @param config Optional custom sanitization config
|
|
323
|
+
* @returns Sanitized HTML
|
|
324
|
+
*/
|
|
325
|
+
html: (input, config) => sanitization.sanitizeHtml(input, config),
|
|
326
|
+
/**
|
|
327
|
+
* Sanitize URLs
|
|
328
|
+
* @param input URL to sanitize
|
|
329
|
+
* @param allowedProtocols Allowed URL protocols
|
|
330
|
+
* @returns Sanitized URL
|
|
331
|
+
*/
|
|
332
|
+
url: (input, allowedProtocols = ['http', 'https']) => sanitization.sanitizeUrl(input, allowedProtocols),
|
|
333
|
+
/**
|
|
334
|
+
* Sanitize file paths to prevent path traversal attacks
|
|
335
|
+
* @param input Path to sanitize
|
|
336
|
+
* @param options Options for path sanitization
|
|
337
|
+
* @returns Sanitized and normalized path
|
|
338
|
+
*/
|
|
339
|
+
path: (input, options = {}) => sanitization.sanitizePath(input, options),
|
|
340
|
+
/**
|
|
341
|
+
* Sanitize a JSON string
|
|
342
|
+
* @param input JSON string to sanitize
|
|
343
|
+
* @param maxSize Maximum allowed size in bytes
|
|
344
|
+
* @returns Parsed and sanitized object
|
|
345
|
+
*/
|
|
346
|
+
json: (input, maxSize) => sanitization.sanitizeJson(input, maxSize),
|
|
347
|
+
/**
|
|
348
|
+
* Ensure input is within a numeric range
|
|
349
|
+
* @param input Number to validate
|
|
350
|
+
* @param min Minimum allowed value
|
|
351
|
+
* @param max Maximum allowed value
|
|
352
|
+
* @returns Sanitized number within range
|
|
353
|
+
*/
|
|
354
|
+
number: (input, min, max) => sanitization.sanitizeNumber(input, min, max)
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Sanitize input for logging to protect sensitive information
|
|
358
|
+
* @param input Input to sanitize
|
|
359
|
+
* @returns Sanitized input safe for logging
|
|
360
|
+
*/
|
|
361
|
+
export const sanitizeInputForLogging = (input) => sanitization.sanitizeForLogging(input);
|
|
362
|
+
// Export default
|
|
363
|
+
export default {
|
|
364
|
+
sanitization,
|
|
365
|
+
sanitizeInput,
|
|
366
|
+
sanitizeInputForLogging
|
|
367
|
+
};
|