pms-analysis-reports-mcp-server
Version:
PMS analysis reports server handling maintenance reports, equipment analysis, compliance tracking, and performance metrics with ERP access for data extraction
178 lines • 6.98 kB
JavaScript
import { logger } from "../utils/logger.js";
import { ObjectId } from 'mongodb';
export class SanitizationMiddleware {
// Sanitize MongoDB query objects to prevent NoSQL injection
static sanitizeMongoQuery(query, options = {}) {
const { allowedFields, maxStringLength = 1000 } = options;
if (query === null || query === undefined) {
return query;
}
if (typeof query === 'string') {
return this.sanitizeString(query, maxStringLength);
}
if (typeof query === 'number') {
return this.sanitizeNumber(query);
}
if (query instanceof Date) {
return query;
}
if (query instanceof ObjectId) {
return options.allowObjectIds ? query : query.toString();
}
if (Array.isArray(query)) {
return query.map(item => this.sanitizeMongoQuery(item, options));
}
if (typeof query === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(query)) {
// Check for dangerous MongoDB operators
if (this.isDangerousOperator(key)) {
logger.warn(`Blocked dangerous MongoDB operator: ${key}`);
continue;
}
// Filter allowed fields if specified
if (allowedFields && !allowedFields.includes(key)) {
logger.warn(`Blocked unauthorized field: ${key}`);
continue;
}
// Recursively sanitize nested objects
sanitized[this.sanitizeString(key, 100)] = this.sanitizeMongoQuery(value, options);
}
return sanitized;
}
return query;
}
// Check for dangerous MongoDB operators
static isDangerousOperator(key) {
const dangerousOperators = [
'$where', // JavaScript execution
'$eval', // JavaScript evaluation
'$function', // Custom functions
'$accumulator', // Custom accumulator functions
'$expr', // Can contain $function
// Allow common safe operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $and, $or, $not, $exists
];
return dangerousOperators.includes(key);
}
// Sanitize string inputs
static sanitizeString(input, maxLength = 1000) {
if (typeof input !== 'string') {
return String(input).slice(0, maxLength);
}
return input
.slice(0, maxLength) // Limit length
.replace(/[\x00-\x1F\x7F]/g, '') // Remove control characters
.replace(/[<>]/g, '') // Remove HTML-like tags
.trim();
}
// Sanitize numeric inputs
static sanitizeNumber(input) {
const num = Number(input);
if (isNaN(num) || !isFinite(num)) {
return null;
}
// Clamp to safe integer range
return Math.max(-Number.MAX_SAFE_INTEGER, Math.min(Number.MAX_SAFE_INTEGER, num));
}
// Sanitize search query for Typesense
static sanitizeSearchQuery(query) {
if (typeof query !== 'string') {
return '';
}
return query
.slice(0, 500) // Limit search query length
.replace(/[^\w\s\-_.,;:()]/g, ' ') // Allow only safe characters
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
}
// Sanitize file paths to prevent directory traversal
static sanitizeFilePath(path) {
if (typeof path !== 'string') {
return '';
}
return path
.replace(/\.\./g, '') // Remove parent directory references
.replace(/[<>:"|?*]/g, '') // Remove invalid filename characters
.replace(/^\/+/, '') // Remove leading slashes
.slice(0, 255); // Limit path length
}
// Sanitize user input for logging (remove sensitive data)
static sanitizeForLogging(data) {
if (data === null || data === undefined) {
return data;
}
if (typeof data === 'string') {
// Mask potential sensitive patterns
return data
.replace(/password[=:]\s*\S+/gi, 'password=***')
.replace(/api[_-]?key[=:]\s*\S+/gi, 'api_key=***')
.replace(/token[=:]\s*\S+/gi, 'token=***')
.replace(/mongodb:\/\/[^@]*@/g, 'mongodb://***@');
}
if (Array.isArray(data)) {
return data.map(item => this.sanitizeForLogging(item));
}
if (typeof data === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
const lowerKey = key.toLowerCase();
if (lowerKey.includes('password') ||
lowerKey.includes('secret') ||
lowerKey.includes('token') ||
lowerKey.includes('key')) {
sanitized[key] = '***';
}
else {
sanitized[key] = this.sanitizeForLogging(value);
}
}
return sanitized;
}
return data;
}
// Sanitize error messages to prevent information leakage
static sanitizeErrorMessage(error) {
if (!error) {
return 'An unknown error occurred';
}
let message = '';
if (typeof error === 'string') {
message = error;
}
else if (error.message) {
message = error.message;
}
else {
message = 'An error occurred';
}
// Remove sensitive information patterns
message = message
.replace(/mongodb:\/\/[^@]*@[^\/]+/g, 'mongodb://***@***/') // Hide DB credentials
.replace(/apikey[=:]\s*\S+/gi, 'apikey=***') // Hide API keys
.replace(/password[=:]\s*\S+/gi, 'password=***') // Hide passwords
.replace(/\/[a-zA-Z]:[\\\/][^\\\/\s]*/g, '/***/') // Hide file paths
.slice(0, 200); // Limit message length
return message || 'An error occurred';
}
// Comprehensive sanitization for tool responses
static sanitizeToolResponse(response) {
if (response === null || response === undefined) {
return response;
}
if (typeof response === 'string') {
return this.sanitizeString(response, 10000); // Allow longer responses
}
if (Array.isArray(response)) {
return response.map(item => this.sanitizeToolResponse(item));
}
if (typeof response === 'object') {
const sanitized = {};
for (const [key, value] of Object.entries(response)) {
sanitized[key] = this.sanitizeToolResponse(value);
}
return sanitized;
}
return response;
}
}
//# sourceMappingURL=sanitization-middleware.js.map