voyage-and-consumption-mcp-server
Version:
Voyage and consumption management server handling vessel voyages, fuel consumption, performance monitoring, and operational data with ERP access for data extraction
309 lines • 11.2 kB
JavaScript
import { logger } from '../utils/logger.js';
export class SanitizationMiddleware {
/**
* Sanitizes input parameters to prevent MongoDB injection attacks
* @param input - The input to sanitize
* @param options - Sanitization options
* @returns Sanitized input
*/
static sanitizeInput(input, options = {}) {
const { allowRegExp = false, maxStringLength = 10000, allowedOperators = [], preventJavaScriptInjection = true } = options;
if (input === null || input === undefined) {
return input;
}
// Handle different types
if (typeof input === 'string') {
return this.sanitizeString(input, maxStringLength, preventJavaScriptInjection);
}
if (typeof input === 'number' || typeof input === 'boolean') {
return input;
}
if (Array.isArray(input)) {
return input.map(item => this.sanitizeInput(item, options));
}
if (typeof input === 'object') {
return this.sanitizeObject(input, options);
}
return input;
}
/**
* Sanitizes string input
*/
static sanitizeString(input, maxLength, preventJavaScript) {
// Length check
if (input.length > maxLength) {
logger.warn(`String length ${input.length} exceeds maximum ${maxLength}, truncating`);
input = input.substring(0, maxLength);
}
// Remove null bytes
input = input.replace(/\0/g, '');
// Prevent JavaScript injection if required
if (preventJavaScript) {
// Remove dangerous patterns
for (const pattern of this.DANGEROUS_PATTERNS) {
input = input.replace(pattern, '');
}
}
return input;
}
/**
* Sanitizes object input
*/
static sanitizeObject(input, options) {
const sanitized = {};
for (const [key, value] of Object.entries(input)) {
// Check for dangerous operators
if (this.isDangerousOperator(key, options.allowedOperators)) {
logger.warn(`Dangerous operator detected and removed: ${key}`);
continue;
}
// Sanitize key
const sanitizedKey = this.sanitizeObjectKey(key);
if (sanitizedKey !== key) {
logger.warn(`Object key sanitized: ${key} -> ${sanitizedKey}`);
}
// Recursively sanitize value
sanitized[sanitizedKey] = this.sanitizeInput(value, options);
}
return sanitized;
}
/**
* Sanitizes object keys
*/
static sanitizeObjectKey(key) {
// Remove dangerous characters
return key.replace(/[^\w$.-]/g, '');
}
/**
* Checks if an operator is dangerous
*/
static isDangerousOperator(operator, allowedOperators = []) {
// If it's in the allowed list, it's safe
if (allowedOperators.includes(operator)) {
return false;
}
// Check if it's a dangerous operator
return this.DANGEROUS_OPERATORS.includes(operator);
}
/**
* Sanitizes MongoDB query parameters
*/
static sanitizeMongoQuery(query, options = {}) {
const sanitizationOptions = {
allowRegExp: false,
maxStringLength: 1000,
allowedOperators: ['$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin', '$and', '$or', '$exists'],
preventJavaScriptInjection: true,
...options
};
return this.sanitizeInput(query, sanitizationOptions);
}
/**
* Sanitizes RegExp patterns to prevent injection
*/
static sanitizeRegExpPattern(pattern) {
// Remove dangerous regex patterns
const dangerousRegexPatterns = [
/\(\?\(/g, // Conditional expressions
/\(\?\=/g, // Positive lookahead
/\(\?\!/g, // Negative lookahead
/\(\?\</g, // Positive lookbehind
/\(\?\!/g, // Negative lookbehind
/\(\?\>/g, // Atomic groups
/\(\?\#/g, // Comments
/\(\?\:/g, // Non-capturing groups (some contexts)
];
let sanitized = pattern;
for (const dangerousPattern of dangerousRegexPatterns) {
sanitized = sanitized.replace(dangerousPattern, '');
}
// Escape special regex characters to prevent injection
sanitized = sanitized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return sanitized;
}
/**
* Creates a safe regex pattern for MongoDB queries
*/
static createSafeRegex(pattern, flags = 'i') {
const sanitizedPattern = this.sanitizeRegExpPattern(pattern);
return new RegExp(sanitizedPattern, flags);
}
/**
* Sanitizes Typesense query parameters
*/
static sanitizeTypesenseQuery(query) {
const sanitized = {};
for (const [key, value] of Object.entries(query)) {
// Sanitize key
const sanitizedKey = key.replace(/[^\w.-]/g, '');
// Sanitize value based on type
if (typeof value === 'string') {
// For Typesense queries, we need to be more permissive but still safe
sanitized[sanitizedKey] = this.sanitizeTypesenseString(value);
}
else if (Array.isArray(value)) {
sanitized[sanitizedKey] = value.map(item => typeof item === 'string' ? this.sanitizeTypesenseString(item) : item);
}
else {
sanitized[sanitizedKey] = value;
}
}
return sanitized;
}
/**
* Sanitizes Typesense search strings
*/
static sanitizeTypesenseString(input) {
// Remove dangerous patterns but allow search operators
let sanitized = input;
// Remove JavaScript injection patterns
for (const pattern of this.DANGEROUS_PATTERNS) {
sanitized = sanitized.replace(pattern, '');
}
// Remove null bytes
sanitized = sanitized.replace(/\0/g, '');
// Limit length
if (sanitized.length > 1000) {
sanitized = sanitized.substring(0, 1000);
}
return sanitized;
}
/**
* Sanitizes URL parameters
*/
static sanitizeUrlParams(params) {
const sanitized = {};
for (const [key, value] of Object.entries(params)) {
// Sanitize key
const sanitizedKey = encodeURIComponent(key);
// Sanitize value
if (typeof value === 'string') {
sanitized[sanitizedKey] = encodeURIComponent(value);
}
else if (typeof value === 'number' || typeof value === 'boolean') {
sanitized[sanitizedKey] = value;
}
else if (Array.isArray(value)) {
sanitized[sanitizedKey] = value.map(item => typeof item === 'string' ? encodeURIComponent(item) : item);
}
else {
sanitized[sanitizedKey] = String(value);
}
}
return sanitized;
}
/**
* Validates and sanitizes date strings
*/
static sanitizeDateString(dateString) {
// Remove any non-date characters
const sanitized = dateString.replace(/[^\d-T:Z.]/g, '');
// Validate format
if (!/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?)?$/.test(sanitized)) {
throw new Error(`Invalid date format: ${dateString}`);
}
return sanitized;
}
/**
* Sanitizes IMO numbers
*/
static sanitizeIMO(imo) {
const imoString = String(imo);
// Remove non-numeric characters
const sanitized = imoString.replace(/[^\d]/g, '');
// Validate length (IMO numbers are 7 digits)
if (sanitized.length !== 7) {
throw new Error(`Invalid IMO format: ${imo}`);
}
return sanitized;
}
/**
* Sanitizes coordinate values
*/
static sanitizeCoordinate(coordinate, type) {
if (typeof coordinate !== 'number' || isNaN(coordinate) || !isFinite(coordinate)) {
throw new Error(`Invalid coordinate value: ${coordinate}`);
}
const min = type === 'latitude' ? -90 : -180;
const max = type === 'latitude' ? 90 : 180;
if (coordinate < min || coordinate > max) {
throw new Error(`${type} ${coordinate} is outside valid range [${min}, ${max}]`);
}
return coordinate;
}
/**
* Sanitizes HTML content to prevent XSS
*/
static sanitizeHtml(html) {
// Simple HTML sanitization - remove script tags and event handlers
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
.replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, '')
.replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, '')
.replace(/on\w+\s*=\s*"[^"]*"/gi, '')
.replace(/on\w+\s*=\s*'[^']*'/gi, '')
.replace(/javascript:/gi, '');
}
/**
* Validates that a query object is safe
*/
static validateQuerySafety(query) {
const queryString = JSON.stringify(query);
// Check for dangerous patterns
for (const pattern of this.DANGEROUS_PATTERNS) {
if (pattern.test(queryString)) {
logger.warn(`Dangerous pattern detected in query: ${pattern}`);
return false;
}
}
// Check for dangerous operators
for (const operator of this.DANGEROUS_OPERATORS) {
if (queryString.includes(operator)) {
logger.warn(`Dangerous operator detected in query: ${operator}`);
return false;
}
}
return true;
}
/**
* Logs security events
*/
static logSecurityEvent(event, details) {
logger.warn(`Security event: ${event}`, {
event,
details,
timestamp: new Date().toISOString()
});
}
}
SanitizationMiddleware.MONGODB_OPERATORS = [
'$eq', '$gt', '$gte', '$in', '$lt', '$lte', '$ne', '$nin',
'$and', '$not', '$nor', '$or',
'$exists', '$type',
'$expr', '$jsonSchema', '$mod', '$regex', '$text', '$where',
'$geoIntersects', '$geoWithin', '$near', '$nearSphere',
'$all', '$elemMatch', '$size',
'$bitsAllClear', '$bitsAllSet', '$bitsAnyClear', '$bitsAnySet'
];
SanitizationMiddleware.DANGEROUS_OPERATORS = [
'$where', '$expr', '$jsonSchema', '$function', '$accumulator', '$regex'
];
SanitizationMiddleware.DANGEROUS_PATTERNS = [
/\$where/gi,
/\$expr/gi,
/\$jsonSchema/gi,
/\$function/gi,
/\$accumulator/gi,
/function\s*\(/gi,
/eval\s*\(/gi,
/setTimeout\s*\(/gi,
/setInterval\s*\(/gi,
/new\s+Function/gi,
/javascript:/gi,
/<script/gi,
/on\w+\s*=/gi
];
// Export singleton for convenience
export const sanitizationMiddleware = SanitizationMiddleware;
//# sourceMappingURL=sanitization-middleware.js.map