survey-mcp-server
Version:
Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management
319 lines • 11.1 kB
JavaScript
import { logger } from '../utils/logger.js';
export class SanitizationMiddleware {
constructor() { }
static getInstance() {
if (!SanitizationMiddleware.instance) {
SanitizationMiddleware.instance = new SanitizationMiddleware();
}
return SanitizationMiddleware.instance;
}
sanitizeInput(input, options = {}) {
const defaultOptions = {
allowHtml: false,
stripScripts: true,
maxStringLength: 10000,
allowedTags: [],
allowedAttributes: []
};
const sanitizationOptions = { ...defaultOptions, ...options };
try {
return this.sanitizeValue(input, sanitizationOptions);
}
catch (error) {
logger.error('Sanitization error:', error);
throw new Error('Input sanitization failed');
}
}
sanitizeValue(value, options) {
if (value === null || value === undefined) {
return value;
}
switch (typeof value) {
case 'string':
return this.sanitizeString(value, options);
case 'number':
return this.sanitizeNumber(value);
case 'boolean':
return value;
case 'object':
if (Array.isArray(value)) {
return this.sanitizeArray(value, options);
}
else {
return this.sanitizeObject(value, options);
}
default:
return value;
}
}
sanitizeString(str, options) {
if (typeof str !== 'string') {
return str;
}
let sanitized = str;
// Trim whitespace
sanitized = sanitized.trim();
// Limit string length
if (options.maxStringLength && sanitized.length > options.maxStringLength) {
sanitized = sanitized.substring(0, options.maxStringLength);
}
// Remove or escape dangerous characters
if (!options.allowHtml) {
sanitized = this.escapeHtml(sanitized);
}
else {
sanitized = this.sanitizeHtml(sanitized, options);
}
// Remove script tags and javascript: URLs
if (options.stripScripts) {
sanitized = this.removeScripts(sanitized);
}
// Remove null bytes and other control characters
sanitized = this.removeControlCharacters(sanitized);
// Prevent path traversal
sanitized = this.preventPathTraversal(sanitized);
return sanitized;
}
sanitizeNumber(num) {
if (typeof num !== 'number') {
return num;
}
// Check for NaN and Infinity
if (isNaN(num) || !isFinite(num)) {
return 0;
}
// Limit to safe integer range
if (num > Number.MAX_SAFE_INTEGER) {
return Number.MAX_SAFE_INTEGER;
}
if (num < Number.MIN_SAFE_INTEGER) {
return Number.MIN_SAFE_INTEGER;
}
return num;
}
sanitizeArray(arr, options) {
if (!Array.isArray(arr)) {
return arr;
}
// Limit array size
const maxArraySize = 1000;
if (arr.length > maxArraySize) {
logger.warn(`Array size ${arr.length} exceeds maximum ${maxArraySize}, truncating`);
arr = arr.slice(0, maxArraySize);
}
return arr.map(item => this.sanitizeValue(item, options));
}
sanitizeObject(obj, options) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// Prevent prototype pollution
if (this.isPrototypePollution(obj)) {
logger.warn('Prototype pollution attempt detected and blocked');
return {};
}
const sanitized = {};
const maxObjectKeys = 100;
let keyCount = 0;
for (const [key, value] of Object.entries(obj)) {
// Limit object size
if (keyCount >= maxObjectKeys) {
logger.warn(`Object key count ${Object.keys(obj).length} exceeds maximum ${maxObjectKeys}, truncating`);
break;
}
// Sanitize key
const sanitizedKey = this.sanitizeString(key, { ...options, allowHtml: false });
// Skip dangerous keys
if (this.isDangerousKey(sanitizedKey)) {
continue;
}
// Sanitize value
sanitized[sanitizedKey] = this.sanitizeValue(value, options);
keyCount++;
}
return sanitized;
}
escapeHtml(str) {
const htmlEscapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return str.replace(/[&<>"'/]/g, (match) => htmlEscapes[match]);
}
sanitizeHtml(str, options) {
const allowedTags = options.allowedTags || [];
const allowedAttributes = options.allowedAttributes || [];
// Remove script tags and event handlers
str = str.replace(/<script[\s\S]*?<\/script>/gi, '');
str = str.replace(/on\w+="[^"]*"/gi, '');
str = str.replace(/on\w+='[^']*'/gi, '');
str = str.replace(/javascript:/gi, '');
// If no allowed tags, escape all HTML
if (allowedTags.length === 0) {
return this.escapeHtml(str);
}
// Simple tag whitelist implementation
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/gi;
return str.replace(tagRegex, (match, tagName) => {
if (allowedTags.includes(tagName.toLowerCase())) {
// Further sanitize attributes if needed
return this.sanitizeAttributes(match, allowedAttributes);
}
return '';
});
}
sanitizeAttributes(tagStr, allowedAttributes) {
if (allowedAttributes.length === 0) {
// Remove all attributes
return tagStr.replace(/\s+[a-zA-Z][a-zA-Z0-9]*="[^"]*"/gi, '');
}
// Simple attribute whitelist implementation
const attrRegex = /\s+([a-zA-Z][a-zA-Z0-9]*)="[^"]*"/gi;
return tagStr.replace(attrRegex, (match, attrName) => {
if (allowedAttributes.includes(attrName.toLowerCase())) {
return match;
}
return '';
});
}
removeScripts(str) {
// Remove script tags
str = str.replace(/<script[\s\S]*?<\/script>/gi, '');
// Remove javascript: URLs
str = str.replace(/javascript:/gi, '');
// Remove event handlers
str = str.replace(/on\w+\s*=\s*"[^"]*"/gi, '');
str = str.replace(/on\w+\s*=\s*'[^']*'/gi, '');
// Remove data: URLs with javascript
str = str.replace(/data:\s*[^;]*;base64\s*,\s*[^"']*/gi, '');
return str;
}
removeControlCharacters(str) {
// Remove null bytes and other control characters except newlines and tabs
return str.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
}
preventPathTraversal(str) {
// Remove path traversal attempts
str = str.replace(/\.\.\//g, '');
str = str.replace(/\.\.\\/g, '');
str = str.replace(/\.\./g, '');
return str;
}
isPrototypePollution(obj) {
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
for (const key of dangerousKeys) {
if (key in obj) {
return true;
}
}
return false;
}
isDangerousKey(key) {
const dangerousKeys = [
'__proto__',
'constructor',
'prototype',
'eval',
'function',
'require',
'import',
'export',
'process',
'global',
'window',
'document'
];
return dangerousKeys.includes(key.toLowerCase());
}
sanitizeMongoQuery(query) {
// Specific sanitization for MongoDB queries
if (typeof query !== 'object' || query === null) {
return query;
}
const sanitized = {};
for (const [key, value] of Object.entries(query)) {
// Remove dangerous MongoDB operators
if (key.startsWith('$') && this.isDangerousMongoOperator(key)) {
continue;
}
// Sanitize regex patterns
if (key === '$regex' || (typeof value === 'object' && value !== null && '$regex' in value)) {
sanitized[key] = this.sanitizeRegex(value);
}
else {
sanitized[key] = this.sanitizeValue(value, { allowHtml: false });
}
}
return sanitized;
}
isDangerousMongoOperator(operator) {
const dangerousOperators = [
'$where',
'$eval',
'$function',
'$accumulator',
'$facet'
];
return dangerousOperators.includes(operator);
}
sanitizeRegex(regexValue) {
if (typeof regexValue === 'string') {
// Escape dangerous regex characters
return regexValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
if (typeof regexValue === 'object' && regexValue !== null && '$regex' in regexValue) {
return {
...regexValue,
$regex: this.sanitizeRegex(regexValue.$regex)
};
}
return regexValue;
}
sanitizeResponse(response) {
// Sanitize response data to prevent information leakage
if (typeof response !== 'object' || response === null) {
return response;
}
const sanitized = {};
for (const [key, value] of Object.entries(response)) {
// Remove sensitive fields
if (this.isSensitiveField(key)) {
continue;
}
// Recursively sanitize nested objects
if (typeof value === 'object' && value !== null) {
sanitized[key] = this.sanitizeResponse(value);
}
else if (typeof value === 'string') {
// Sanitize string values
sanitized[key] = this.sanitizeString(value, { allowHtml: false });
}
else {
sanitized[key] = value;
}
}
return sanitized;
}
isSensitiveField(fieldName) {
const sensitiveFields = [
'password',
'secret',
'token',
'key',
'credential',
'auth',
'session',
'cookie',
'private',
'internal'
];
const fieldLower = fieldName.toLowerCase();
return sensitiveFields.some(sensitive => fieldLower.includes(sensitive));
}
}
export const sanitizationMiddleware = SanitizationMiddleware.getInstance();
//# sourceMappingURL=sanitization.js.map