survey-mcp-server
Version:
Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management
298 lines • 11.3 kB
JavaScript
import { logger } from '../utils/logger.js';
export class SecuritySanitizer {
constructor() { }
static getInstance() {
if (!SecuritySanitizer.instance) {
SecuritySanitizer.instance = new SecuritySanitizer();
}
return SecuritySanitizer.instance;
}
sanitizeForMongoDB(input) {
try {
return this.deepSanitizeObject(input, this.mongoDBSanitizer.bind(this));
}
catch (error) {
logger.error('MongoDB sanitization error:', error);
throw new Error('MongoDB sanitization failed');
}
}
sanitizeForTypesense(input) {
try {
return this.deepSanitizeObject(input, this.typesenseSanitizer.bind(this));
}
catch (error) {
logger.error('Typesense sanitization error:', error);
throw new Error('Typesense sanitization failed');
}
}
sanitizeForExternalAPI(input) {
try {
return this.deepSanitizeObject(input, this.externalAPISanitizer.bind(this));
}
catch (error) {
logger.error('External API sanitization error:', error);
throw new Error('External API sanitization failed');
}
}
sanitizeCredentials(credentials) {
const sanitized = { ...credentials };
// Remove or mask sensitive fields
const sensitiveFields = ['password', 'secret', 'token', 'key', 'apiKey', 'accessToken'];
for (const field of sensitiveFields) {
if (field in sanitized) {
if (typeof sanitized[field] === 'string' && sanitized[field].length > 0) {
// Keep first 2 and last 2 characters, mask the rest
const value = sanitized[field];
if (value.length > 4) {
sanitized[field] = value.substring(0, 2) + '*'.repeat(value.length - 4) + value.substring(value.length - 2);
}
else {
sanitized[field] = '*'.repeat(value.length);
}
}
else {
sanitized[field] = '[REDACTED]';
}
}
}
return sanitized;
}
deepSanitizeObject(obj, sanitizer) {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
return sanitizer(obj);
}
if (typeof obj === 'number' || typeof obj === 'boolean') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => this.deepSanitizeObject(item, sanitizer));
}
if (typeof obj === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(obj)) {
// Sanitize the key
const sanitizedKey = sanitizer(key);
// Skip dangerous keys
if (this.isDangerousKey(sanitizedKey)) {
continue;
}
// Recursively sanitize the value
sanitized[sanitizedKey] = this.deepSanitizeObject(value, sanitizer);
}
return sanitized;
}
return obj;
}
mongoDBSanitizer(value) {
if (typeof value !== 'string') {
return value;
}
let sanitized = value;
// Remove dangerous MongoDB operators
const dangerousOperators = [
'$where',
'$eval',
'$function',
'$accumulator',
'$facet',
'$expr'
];
for (const operator of dangerousOperators) {
const regex = new RegExp(`\\${operator}`, 'gi');
sanitized = sanitized.replace(regex, '');
}
// Escape special regex characters if it looks like a regex
if (sanitized.includes('$regex')) {
sanitized = this.escapeRegexSpecialChars(sanitized);
}
// Remove potential NoSQL injection patterns
const injectionPatterns = [
/\{\s*\$ne\s*:\s*null\s*\}/gi,
/\{\s*\$gt\s*:\s*""\s*\}/gi,
/\{\s*\$regex\s*:\s*".*"\s*\}/gi,
/\{\s*\$exists\s*:\s*true\s*\}/gi
];
for (const pattern of injectionPatterns) {
sanitized = sanitized.replace(pattern, '');
}
return sanitized;
}
typesenseSanitizer(value) {
if (typeof value !== 'string') {
return value;
}
let sanitized = value;
// Remove or escape Typesense special characters
const specialChars = ['*', '?', ':', '(', ')', '[', ']', '{', '}', '^', '~'];
for (const char of specialChars) {
const regex = new RegExp(`\\${char}`, 'g');
sanitized = sanitized.replace(regex, `\\${char}`);
}
// Remove potential script injections
sanitized = this.removeScriptContent(sanitized);
return sanitized;
}
externalAPISanitizer(value) {
if (typeof value !== 'string') {
return value;
}
let sanitized = value;
// Remove script content
sanitized = this.removeScriptContent(sanitized);
// Remove event handlers
sanitized = this.removeEventHandlers(sanitized);
// Remove dangerous URLs
sanitized = this.removeDangerousUrls(sanitized);
// Limit string length
if (sanitized.length > 10000) {
sanitized = sanitized.substring(0, 10000);
}
return sanitized;
}
removeScriptContent(str) {
// Remove script tags and their content
str = str.replace(/<script[\s\S]*?<\/script>/gi, '');
// Remove javascript: URLs
str = str.replace(/javascript\s*:/gi, '');
// Remove vbscript: URLs
str = str.replace(/vbscript\s*:/gi, '');
// Remove data: URLs with executable content
str = str.replace(/data\s*:\s*[^;]*;base64\s*,\s*[^"']*/gi, '');
str = str.replace(/data\s*:\s*text\/html[^"']*/gi, '');
return str;
}
removeEventHandlers(str) {
// Remove HTML event handlers
const eventHandlers = [
'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
'onkeydown', 'onkeyup', 'onkeypress', 'onfocus', 'onblur', 'onchange', 'onsubmit',
'onreset', 'onselect', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange',
'onemptied', 'onended', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause',
'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking',
'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting'
];
for (const handler of eventHandlers) {
const regex = new RegExp(`${handler}\\s*=\\s*["'][^"']*["']`, 'gi');
str = str.replace(regex, '');
}
return str;
}
removeDangerousUrls(str) {
// Remove dangerous protocol URLs
const dangerousProtocols = [
'javascript:',
'vbscript:',
'data:',
'file:',
'ftp:',
'jar:',
'chrome:',
'chrome-extension:',
'moz-extension:'
];
for (const protocol of dangerousProtocols) {
const regex = new RegExp(protocol.replace(':', '\\s*:'), 'gi');
str = str.replace(regex, '');
}
return str;
}
escapeRegexSpecialChars(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
isDangerousKey(key) {
const dangerousKeys = [
'__proto__',
'constructor',
'prototype',
'eval',
'function',
'require',
'import',
'process',
'global',
'window',
'document'
];
return dangerousKeys.includes(key.toLowerCase());
}
sanitizeLogData(data) {
// Sanitize data for logging to prevent log injection
const sanitized = this.deepSanitizeObject(data, (value) => {
if (typeof value !== 'string') {
return value;
}
// Remove newlines and carriage returns to prevent log injection
let clean = value.replace(/[\r\n]/g, ' ');
// Remove ANSI escape sequences
clean = clean.replace(/\x1b\[[0-9;]*m/g, '');
// Remove other control characters
clean = clean.replace(/[\x00-\x1F\x7F]/g, '');
// Limit length
if (clean.length > 1000) {
clean = clean.substring(0, 1000) + '...';
}
return clean;
});
// Remove sensitive fields
return this.removeSensitiveFields(sanitized);
}
removeSensitiveFields(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => this.removeSensitiveFields(item));
}
const sanitized = {};
const sensitiveFields = [
'password', 'secret', 'token', 'key', 'apikey', 'accesstoken',
'credential', 'auth', 'session', 'cookie', 'private'
];
for (const [key, value] of Object.entries(obj)) {
const keyLower = key.toLowerCase();
const isSensitive = sensitiveFields.some(field => keyLower.includes(field));
if (isSensitive) {
sanitized[key] = '[REDACTED]';
}
else if (typeof value === 'object') {
sanitized[key] = this.removeSensitiveFields(value);
}
else {
sanitized[key] = value;
}
}
return sanitized;
}
sanitizeError(error) {
// Sanitize error objects to prevent sensitive information leakage
const sanitized = {
message: error.message || 'An error occurred',
name: error.name || 'Error',
timestamp: new Date().toISOString()
};
// Include safe properties
if (error.code && typeof error.code === 'string') {
sanitized.code = error.code;
}
if (error.statusCode && typeof error.statusCode === 'number') {
sanitized.statusCode = error.statusCode;
}
// Sanitize the error message
if (typeof sanitized.message === 'string') {
// Remove file paths
sanitized.message = sanitized.message.replace(/\/[^\s]+/g, '[PATH]');
// Remove potential credentials
sanitized.message = sanitized.message.replace(/password[=:]\s*[^\s]+/gi, 'password=[REDACTED]');
sanitized.message = sanitized.message.replace(/token[=:]\s*[^\s]+/gi, 'token=[REDACTED]');
sanitized.message = sanitized.message.replace(/key[=:]\s*[^\s]+/gi, 'key=[REDACTED]');
// Remove MongoDB connection strings
sanitized.message = sanitized.message.replace(/mongodb:\/\/[^@]*@[^\/]+/gi, 'mongodb://[REDACTED]@[HOST]');
}
return sanitized;
}
}
export const securitySanitizer = SecuritySanitizer.getInstance();
//# sourceMappingURL=sanitization.js.map