UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

350 lines 12.3 kB
/** * Input Validator * Comprehensive input validation and sanitization system */ import { PathTraversalPrevention } from './path-traversal-prevention'; import { SqlInjectionPrevention } from './sql-injection-prevention'; export class InputValidator { config; customRules = new Map(); constructor(config = {}) { this.config = { enableSqlInjectionPrevention: config.enableSqlInjectionPrevention ?? true, enablePathTraversalPrevention: config.enablePathTraversalPrevention ?? true, enableXssProtection: config.enableXssProtection ?? true, enableRateLimiting: config.enableRateLimiting ?? false, customRules: config.customRules ?? [], sanitizationDefaults: { stripHtml: true, escapeHtml: true, trimWhitespace: true, normalizeUnicode: false, maxLength: 10000, allowedChars: /^[\w\s\-_.@#$%^&*()+={}[\]|\\:";'<>?,./`~!]*$/, blockedPatterns: [], ...config.sanitizationDefaults } }; // Register custom rules this.config.customRules.forEach(rule => { this.customRules.set(rule.name, rule); }); // Register built-in rules this.registerBuiltInRules(); } /** * Validates input against a schema */ validate(data, schema) { const errors = []; const sanitizedData = {}; for (const [field, rules] of Object.entries(schema)) { const value = data[field]; let sanitizedValue = value; for (const rule of rules) { try { // Validate the original value first (before sanitization) if (!rule.validate(sanitizedValue)) { errors.push({ field, rule: rule.name, message: rule.message, value: sanitizedValue }); } // Apply sanitization if available if (rule.sanitize) { sanitizedValue = rule.sanitize(sanitizedValue); } } catch (error) { errors.push({ field, rule: rule.name, message: error instanceof Error ? error.message : 'Validation error', value: sanitizedValue }); } } sanitizedData[field] = sanitizedValue; } return { isValid: errors.length === 0, errors, sanitizedData }; } /** * Sanitizes a single value */ sanitize(value, options = {}) { const opts = { ...this.config.sanitizationDefaults, ...options }; if (typeof value !== 'string') { return value; } let sanitized = value; // Trim whitespace if (opts.trimWhitespace) { sanitized = sanitized.trim(); } // Normalize unicode if (opts.normalizeUnicode) { sanitized = sanitized.normalize('NFC'); } // Apply max length if (opts.maxLength && sanitized.length > opts.maxLength) { sanitized = sanitized.substring(0, opts.maxLength); } // Remove blocked patterns if (opts.blockedPatterns) { for (const pattern of opts.blockedPatterns) { sanitized = sanitized.replace(pattern, ''); } } // Filter allowed characters if (opts.allowedChars) { sanitized = sanitized.replace(new RegExp(`[^${opts.allowedChars.source.slice(2, -2)}]`, 'g'), ''); } // HTML handling if (opts.stripHtml) { sanitized = this.stripHtml(sanitized); } else if (opts.escapeHtml) { sanitized = this.escapeHtml(sanitized); } return sanitized; } /** * Validates and sanitizes input for SQL injection */ validateSqlInput(input) { if (!this.config.enableSqlInjectionPrevention) { return { isValid: true, sanitized: input, threats: [] }; } try { const result = SqlInjectionPrevention.validateAndSanitizeInputs(typeof input === 'object' ? input : { value: input }); const sanitized = typeof input === 'object' ? result.sanitizedInputs : result.sanitizedInputs.value ?? input; return { isValid: result.isValid, sanitized, threats: result.threats.flatMap(t => t.threats.map(threat => threat.description)) }; } catch (error) { return { isValid: false, sanitized: input, threats: [error instanceof Error ? error.message : 'SQL validation error'] }; } } /** * Validates path for traversal attacks */ validatePath(path) { if (!this.config.enablePathTraversalPrevention) { return { isValid: true, sanitized: path, errors: [] }; } return PathTraversalPrevention.validatePath(path); } /** * Creates a validation rule */ createRule(name, validate, message, sanitize) { const rule = { name, validate, message, ...(sanitize && { sanitize }) }; this.customRules.set(name, rule); return rule; } /** * Gets a validation rule by name */ getRule(name) { return this.customRules.get(name); } /** * Registers built-in validation rules */ registerBuiltInRules() { // Required field rule this.customRules.set('required', { name: 'required', validate: (value) => value !== null && value !== undefined && value !== '', message: 'This field is required' }); // Email validation rule this.customRules.set('email', { name: 'email', validate: (value) => { if (!value) return true; // Allow empty for optional fields const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(value); }, message: 'Please enter a valid email address', sanitize: (value) => value?.toLowerCase().trim() }); // URL validation rule this.customRules.set('url', { name: 'url', validate: (value) => { if (!value) return true; try { new URL(value); return true; } catch { return false; } }, message: 'Please enter a valid URL' }); // Numeric validation rule this.customRules.set('numeric', { name: 'numeric', validate: (value) => { if (value === null || value === undefined || value === '') return true; return !isNaN(Number(value)); }, message: 'This field must be a number', sanitize: (value) => { const num = Number(value); return isNaN(num) ? value : num; } }); // Integer validation rule this.customRules.set('integer', { name: 'integer', validate: (value) => { if (value === null || value === undefined || value === '') return true; const num = Number(value); return Number.isInteger(num); }, message: 'This field must be an integer', sanitize: (value) => { const num = Number(value); return Number.isInteger(num) ? Math.floor(num) : value; } }); // Length validation rules this.customRules.set('minLength', { name: 'minLength', validate: (value, min = 0) => { if (!value) return true; return value.length >= min; }, message: 'This field is too short' }); this.customRules.set('maxLength', { name: 'maxLength', validate: (value, max = Infinity) => { if (!value) return true; return value.length <= max; }, message: 'This field is too long', sanitize: (value, max = Infinity) => { return value?.substring(0, max); } }); // Pattern validation rule this.customRules.set('pattern', { name: 'pattern', validate: (value) => { if (!value) return true; // Note: This is a simplified pattern validation // In practice, you'd need to pass the pattern differently return true; }, message: 'This field format is invalid' }); // SQL injection prevention rule if (this.config.enableSqlInjectionPrevention) { this.customRules.set('noSqlInjection', { name: 'noSqlInjection', validate: (value) => { if (!value) return true; const result = SqlInjectionPrevention.validateInput(value); return result.isValid; }, message: 'Input contains potentially dangerous SQL patterns', sanitize: (value) => { if (!value) return value; return SqlInjectionPrevention.sanitizeInput(value); } }); } // Path traversal prevention rule if (this.config.enablePathTraversalPrevention) { this.customRules.set('safePath', { name: 'safePath', validate: (value) => { if (!value) return true; const result = PathTraversalPrevention.validatePath(value); return result.isValid; }, message: 'Path contains potentially dangerous patterns', sanitize: (value) => { if (!value) return value; return PathTraversalPrevention.sanitizePath(value); } }); } // XSS prevention rule if (this.config.enableXssProtection) { this.customRules.set('noXss', { name: 'noXss', validate: (value) => { if (!value) return true; // Basic XSS pattern detection const xssPatterns = [ /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, /javascript:/gi, /on\w+\s*=/gi, /<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi ]; return !xssPatterns.some(pattern => pattern.test(value)); }, message: 'Input contains potentially dangerous script content', sanitize: (value) => this.escapeHtml(value) }); } } /** * Strips HTML tags from input */ stripHtml(input) { return input.replace(/<[^>]*>/g, ''); } /** * Escapes HTML characters */ escapeHtml(input) { const htmlEscapes = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;' }; return input.replace(/[&<>"'/]/g, (match) => htmlEscapes[match] || match); } } //# sourceMappingURL=input-validator.js.map