@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
350 lines • 12.3 kB
JavaScript
/**
* 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return input.replace(/[&<>"'/]/g, (match) => htmlEscapes[match] || match);
}
}
//# sourceMappingURL=input-validator.js.map