web-vuln-scanner
Version:
Advanced, lightweight web vulnerability scanner with smart detection and easy-to-use interface
363 lines (309 loc) • 9.96 kB
JavaScript
/**
* Security and Input Validation Module
* Production-ready security utilities for web vulnerability scanner
*/
const crypto = require('crypto');
const { URL } = require('url');
class SecurityValidator {
constructor() {
this.MAX_URL_LENGTH = 2048;
this.MAX_HEADER_LENGTH = 8192;
this.MAX_PAYLOAD_SIZE = 1024 * 1024; // 1MB
this.ALLOWED_PROTOCOLS = ['http:', 'https:'];
this.FORBIDDEN_HEADERS = ['authorization', 'cookie', 'x-forwarded-for'];
this.RATE_LIMIT_WINDOW = 60000; // 1 minute
this.MAX_REQUESTS_PER_WINDOW = 100;
// Rate limiting store
this.rateLimitStore = new Map();
// Cleanup rate limit store every 5 minutes
setInterval(() => this.cleanupRateLimit(), 5 * 60 * 1000);
}
/**
* Validate and sanitize URL input
* @param {string} url - URL to validate
* @returns {object} - Validation result
*/
validateUrl(url) {
const result = {
isValid: false,
sanitized: null,
errors: []
};
if (!url || typeof url !== 'string') {
result.errors.push('URL must be a non-empty string');
return result;
}
// Check length
if (url.length > this.MAX_URL_LENGTH) {
result.errors.push(`URL exceeds maximum length of ${this.MAX_URL_LENGTH} characters`);
return result;
}
// Check for dangerous characters
const dangerousChars = /[\x00-\x1f\x7f-\x9f]/;
if (dangerousChars.test(url)) {
result.errors.push('URL contains dangerous control characters');
return result;
}
try {
const parsedUrl = new URL(url);
// Validate protocol
if (!this.ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
result.errors.push(`Protocol ${parsedUrl.protocol} is not allowed`);
return result;
}
// Block local/private IPs in production
if (this.isLocalOrPrivateIP(parsedUrl.hostname)) {
result.errors.push('Local and private IP addresses are not allowed');
return result;
}
// Sanitize and normalize
result.sanitized = parsedUrl.href;
result.isValid = true;
} catch (error) {
result.errors.push(`Invalid URL format: ${error.message}`);
}
return result;
}
/**
* Check if hostname is local or private IP
* @param {string} hostname
* @returns {boolean}
*/
isLocalOrPrivateIP(hostname) {
// localhost patterns
if (['localhost', '127.0.0.1', '::1'].includes(hostname)) {
return true;
}
// Private IP ranges
const privateRanges = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./, // Link-local
/^fc00::/i, // IPv6 private
/^fe80::/i // IPv6 link-local
];
return privateRanges.some(range => range.test(hostname));
}
/**
* Validate HTTP headers
* @param {object} headers
* @returns {object}
*/
validateHeaders(headers) {
const result = {
isValid: false,
sanitized: {},
errors: []
};
if (!headers || typeof headers !== 'object') {
result.isValid = true;
result.sanitized = {};
return result;
}
for (const [key, value] of Object.entries(headers)) {
// Validate header name
if (typeof key !== 'string' || key.length === 0) {
result.errors.push('Header names must be non-empty strings');
continue;
}
// Check for forbidden headers
if (this.FORBIDDEN_HEADERS.includes(key.toLowerCase())) {
result.errors.push(`Header ${key} is not allowed`);
continue;
}
// Validate header value
if (typeof value !== 'string') {
result.errors.push(`Header ${key} value must be a string`);
continue;
}
// Check length
if (value.length > this.MAX_HEADER_LENGTH) {
result.errors.push(`Header ${key} exceeds maximum length`);
continue;
}
// Sanitize header value (remove dangerous characters)
const sanitizedValue = value.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
result.sanitized[key] = sanitizedValue;
}
result.isValid = result.errors.length === 0;
return result;
}
/**
* Validate scan configuration
* @param {object} config
* @returns {object}
*/
validateScanConfig(config) {
const result = {
isValid: false,
sanitized: {},
errors: []
};
if (!config || typeof config !== 'object') {
result.errors.push('Configuration must be an object');
return result;
}
// Validate timeout
if (config.timeout !== undefined) {
const timeout = parseInt(config.timeout);
if (isNaN(timeout) || timeout < 1000 || timeout > 300000) {
result.errors.push('Timeout must be between 1000ms and 300000ms');
} else {
result.sanitized.timeout = timeout;
}
}
// Validate depth
if (config.depth !== undefined) {
const depth = parseInt(config.depth);
if (isNaN(depth) || depth < 0 || depth > 10) {
result.errors.push('Depth must be between 0 and 10');
} else {
result.sanitized.depth = depth;
}
}
// Validate concurrency
if (config.concurrency !== undefined) {
const concurrency = parseInt(config.concurrency);
if (isNaN(concurrency) || concurrency < 1 || concurrency > 50) {
result.errors.push('Concurrency must be between 1 and 50');
} else {
result.sanitized.concurrency = concurrency;
}
}
// Validate modules
if (config.scanModules) {
if (!Array.isArray(config.scanModules)) {
result.errors.push('Scan modules must be an array');
} else {
const validModules = [
'headers', 'xss', 'sql', 'ssl', 'ports', 'dirTraversal',
'csrf', 'csp', 'versionCheck', 'rce', 'idor',
'misconfiguredHeaders', 'waf'
];
const invalidModules = config.scanModules.filter(m => !validModules.includes(m));
if (invalidModules.length > 0) {
result.errors.push(`Invalid modules: ${invalidModules.join(', ')}`);
} else {
result.sanitized.scanModules = config.scanModules;
}
}
}
// Copy other valid configurations
['disableCrawler', 'enableJS', 'userAgent', 'benchmark'].forEach(key => {
if (config[key] !== undefined) {
result.sanitized[key] = config[key];
}
});
result.isValid = result.errors.length === 0;
return result;
}
/**
* Rate limiting check
* @param {string} identifier - Client identifier
* @returns {boolean} - Whether request is allowed
*/
checkRateLimit(identifier) {
const now = Date.now();
const windowStart = now - this.RATE_LIMIT_WINDOW;
if (!this.rateLimitStore.has(identifier)) {
this.rateLimitStore.set(identifier, []);
}
const requests = this.rateLimitStore.get(identifier);
// Remove old requests outside the window
const recentRequests = requests.filter(timestamp => timestamp > windowStart);
if (recentRequests.length >= this.MAX_REQUESTS_PER_WINDOW) {
return false; // Rate limit exceeded
}
// Add current request
recentRequests.push(now);
this.rateLimitStore.set(identifier, recentRequests);
return true;
}
/**
* Clean up old rate limit entries
*/
cleanupRateLimit() {
const now = Date.now();
const windowStart = now - this.RATE_LIMIT_WINDOW;
for (const [identifier, requests] of this.rateLimitStore) {
const recentRequests = requests.filter(timestamp => timestamp > windowStart);
if (recentRequests.length === 0) {
this.rateLimitStore.delete(identifier);
} else {
this.rateLimitStore.set(identifier, recentRequests);
}
}
}
/**
* Sanitize payload for logging
* @param {string} payload
* @returns {string}
*/
sanitizeForLogging(payload) {
if (!payload || typeof payload !== 'string') return '';
return payload
.replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters
.substring(0, 1000) // Limit length
.replace(/[<>"'&]/g, char => ({ // HTML encode dangerous chars
'<': '<',
'>': '>',
'"': '"',
'\'': ''',
'&': '&'
})[char]);
}
/**
* Generate secure random identifier
* @returns {string}
*/
generateSecureId() {
return crypto.randomBytes(16).toString('hex');
}
/**
* Hash sensitive data for storage
* @param {string} data
* @returns {string}
*/
hashSensitiveData(data) {
return crypto.createHash('sha256').update(data).digest('hex');
}
/**
* Validate file path for output
* @param {string} filePath
* @returns {object}
*/
validateOutputPath(filePath) {
const result = {
isValid: false,
sanitized: null,
errors: []
};
if (!filePath || typeof filePath !== 'string') {
result.errors.push('File path must be a non-empty string');
return result;
}
// Check for path traversal
if (filePath.includes('..') || filePath.includes('~')) {
result.errors.push('Path traversal attempts are not allowed');
return result;
}
// Check for dangerous characters
const dangerousChars = /[\x00-\x1f\x7f-\x9f<>:"|?*]/;
if (dangerousChars.test(filePath)) {
result.errors.push('File path contains dangerous characters');
return result;
}
// Validate file extension
const allowedExtensions = ['.html', '.json', '.md', '.txt', '.xml'];
const hasValidExtension = allowedExtensions.some(ext => filePath.toLowerCase().endsWith(ext));
if (!hasValidExtension) {
result.errors.push(`File extension must be one of: ${allowedExtensions.join(', ')}`);
return result;
}
result.sanitized = filePath;
result.isValid = true;
return result;
}
}
module.exports = { SecurityValidator };