survey-mcp-server
Version:
Survey management server handling survey creation, response collection, analysis, and reporting with database access for data management
308 lines • 10.7 kB
JavaScript
import { logger } from '../utils/logger.js';
export class SecurityValidator {
constructor() { }
static getInstance() {
if (!SecurityValidator.instance) {
SecurityValidator.instance = new SecurityValidator();
}
return SecurityValidator.instance;
}
validateInput(input, options = {}) {
const defaultOptions = {
maxDepth: 10,
maxKeys: 100,
allowedTypes: ['string', 'number', 'boolean', 'object', 'array'],
blacklistedPatterns: [
/eval\s*\(/i,
/function\s*\(/i,
/require\s*\(/i,
/import\s*\(/i,
/process\s*\./i,
/global\s*\./i,
/window\s*\./i,
/document\s*\./i,
/script\s*>/i,
/javascript\s*:/i,
/data\s*:\s*text\/html/i,
/vbscript\s*:/i
]
};
const validationOptions = { ...defaultOptions, ...options };
const issues = [];
try {
this.validateValue(input, validationOptions, issues, 0, 'root');
if (issues.length > 0) {
logger.warn('Security validation issues found:', issues);
}
return {
isValid: issues.length === 0,
issues
};
}
catch (error) {
logger.error('Security validation error:', error);
return {
isValid: false,
issues: [`Validation error: ${error.message}`]
};
}
}
validateValue(value, options, issues, depth, path) {
// Check depth limit
if (depth > (options.maxDepth || 10)) {
issues.push(`Maximum depth exceeded at ${path}`);
return;
}
// Check type
const valueType = Array.isArray(value) ? 'array' : typeof value;
if (options.allowedTypes && !options.allowedTypes.includes(valueType)) {
issues.push(`Disallowed type '${valueType}' at ${path}`);
return;
}
// String-specific validations
if (typeof value === 'string') {
this.validateString(value, options, issues, path);
}
// Object-specific validations
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
this.validateArray(value, options, issues, depth, path);
}
else {
this.validateObject(value, options, issues, depth, path);
}
}
}
validateString(value, options, issues, path) {
// Check for malicious patterns
if (options.blacklistedPatterns) {
for (const pattern of options.blacklistedPatterns) {
if (pattern.test(value)) {
issues.push(`Suspicious pattern detected in string at ${path}: ${pattern.source}`);
}
}
}
// Check for common injection attempts
this.checkForInjectionAttempts(value, issues, path);
// Check for suspicious keywords
this.checkForSuspiciousKeywords(value, issues, path);
}
validateArray(value, options, issues, depth, path) {
// Check array size
if (value.length > 1000) {
issues.push(`Array too large (${value.length} items) at ${path}`);
return;
}
// Validate each item
for (let i = 0; i < value.length; i++) {
this.validateValue(value[i], options, issues, depth + 1, `${path}[${i}]`);
}
}
validateObject(value, options, issues, depth, path) {
const keys = Object.keys(value);
// Check object size
if (keys.length > (options.maxKeys || 100)) {
issues.push(`Object too large (${keys.length} keys) at ${path}`);
return;
}
// Check for prototype pollution
if (this.checkForPrototypePollution(value, issues, path)) {
return; // Stop processing if prototype pollution detected
}
// Validate each property
for (const key of keys) {
// Validate key name
this.validatePropertyKey(key, issues, path);
// Validate value
this.validateValue(value[key], options, issues, depth + 1, `${path}.${key}`);
}
}
checkForInjectionAttempts(value, issues, path) {
// SQL injection patterns
const sqlPatterns = [
/(\s|^)(union|select|insert|update|delete|drop|create|alter)\s/i,
/(\s|^)(or|and)\s+\d+\s*=\s*\d+/i,
/('|"|`)\s*(or|and|union)\s*\1/i,
/;\s*(drop|delete|update|insert)/i
];
// NoSQL injection patterns
const nosqlPatterns = [
/\$where/i,
/\$eval/i,
/\$function/i,
/\$regex.*\$options/i,
/\{\s*\$ne\s*:\s*null\s*\}/i
];
// Command injection patterns
const commandPatterns = [
/;.*(?:rm|del|format|shutdown|reboot)/i,
/\|.*(?:cat|type|more|less)/i,
/&&.*(?:rm|del|format)/i,
/`.*`/,
/\$\(.*\)/
];
const allPatterns = [...sqlPatterns, ...nosqlPatterns, ...commandPatterns];
for (const pattern of allPatterns) {
if (pattern.test(value)) {
issues.push(`Possible injection attempt detected at ${path}: ${pattern.source}`);
}
}
}
checkForSuspiciousKeywords(value, issues, path) {
const suspiciousKeywords = [
'eval',
'function',
'constructor',
'prototype',
'__proto__',
'require',
'import',
'process',
'global',
'window',
'document',
'location',
'navigator',
'cookie',
'localStorage',
'sessionStorage',
'indexedDB',
'webSQL',
'chrome',
'webkitStorageInfo'
];
const valueLower = value.toLowerCase();
for (const keyword of suspiciousKeywords) {
if (valueLower.includes(keyword)) {
issues.push(`Suspicious keyword '${keyword}' found at ${path}`);
}
}
}
validatePropertyKey(key, issues, path) {
// Check for dangerous property names
const dangerousKeys = [
'__proto__',
'constructor',
'prototype',
'eval',
'function',
'require',
'import',
'process',
'global'
];
if (dangerousKeys.includes(key)) {
issues.push(`Dangerous property key '${key}' at ${path}`);
}
// Check for MongoDB operator abuse
if (key.startsWith('$') && !this.isAllowedMongoOperator(key)) {
issues.push(`Potentially dangerous MongoDB operator '${key}' at ${path}`);
}
}
checkForPrototypePollution(obj, issues, path) {
const pollutionKeys = ['__proto__', 'constructor', 'prototype'];
for (const key of pollutionKeys) {
if (key in obj) {
issues.push(`Prototype pollution attempt detected at ${path}.${key}`);
return true;
}
}
return false;
}
isAllowedMongoOperator(operator) {
const allowedOperators = [
// Comparison
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin',
// Logical
'$and', '$or', '$not', '$nor',
// Element
'$exists', '$type',
// Array
'$all', '$elemMatch', '$size',
// Text
'$text', '$search',
// Regex (with caution)
'$regex', '$options'
];
return allowedOperators.includes(operator);
}
validateIMO(imo) {
const issues = [];
// Convert to string
const imoStr = String(imo).trim();
// Check that it contains only digits
if (!/^\d+$/.test(imoStr)) {
issues.push('IMO must contain only digits');
return { isValid: false, issues };
}
return {
isValid: true,
normalizedIMO: imoStr,
issues: []
};
}
validateEmail(email) {
const issues = [];
// Basic email regex (RFC 5322 compliant)
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
if (!emailRegex.test(email)) {
issues.push('Invalid email format');
return { isValid: false, issues };
}
// Check for suspicious patterns in email
const suspiciousPatterns = [
/script/i,
/javascript/i,
/vbscript/i,
/onload/i,
/onerror/i
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(email)) {
issues.push('Email contains suspicious content');
return { isValid: false, issues };
}
}
return {
isValid: true,
normalizedEmail: email.toLowerCase().trim(),
issues: []
};
}
validateURL(url) {
const issues = [];
try {
const urlObj = new URL(url);
// Check for allowed protocols
const allowedProtocols = ['http:', 'https:'];
if (!allowedProtocols.includes(urlObj.protocol)) {
issues.push(`Protocol '${urlObj.protocol}' not allowed`);
return { isValid: false, issues };
}
// Check for suspicious patterns
const suspiciousPatterns = [
/javascript:/i,
/vbscript:/i,
/data:/i,
/file:/i
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(url)) {
issues.push('URL contains suspicious protocol or content');
return { isValid: false, issues };
}
}
return {
isValid: true,
normalizedURL: urlObj.toString(),
issues: []
};
}
catch (error) {
issues.push('Invalid URL format');
return { isValid: false, issues };
}
}
}
export const securityValidator = SecurityValidator.getInstance();
//# sourceMappingURL=validation.js.map