streamflow/backend/utils/inputValidator.js

487 lines
10 KiB
JavaScript
Raw Permalink Normal View History

/**
* Comprehensive Input Validation Utility
* Implements whitelist-based validation for all user inputs
*/
const validator = require('validator');
/**
* Validation Rules Configuration
*/
const VALIDATION_RULES = {
// User-related
username: {
minLength: 3,
maxLength: 50,
pattern: /^[a-zA-Z0-9_-]+$/,
sanitize: true
},
email: {
maxLength: 255,
sanitize: true
},
password: {
minLength: 8,
maxLength: 128
},
// Content-related
playlistName: {
minLength: 1,
maxLength: 200,
pattern: /^[a-zA-Z0-9\s\-_.,()!]+$/,
sanitize: true
},
channelName: {
minLength: 1,
maxLength: 200,
sanitize: true
},
url: {
maxLength: 2048,
protocols: ['http', 'https', 'rtmp', 'rtsp', 'udp', 'rtp']
},
// Generic text fields
description: {
maxLength: 1000,
sanitize: true
},
// File names
filename: {
maxLength: 255,
pattern: /^[a-zA-Z0-9\s\-_.,()]+$/,
sanitize: true
},
// Settings keys
settingKey: {
maxLength: 100,
pattern: /^[a-zA-Z0-9_.-]+$/
}
};
/**
* Sanitize string input to prevent XSS
*/
function sanitizeString(str) {
if (typeof str !== 'string') return str;
// Remove HTML tags
str = str.replace(/<[^>]*>/g, '');
// Remove script-related content
str = str.replace(/javascript:/gi, '');
str = str.replace(/on\w+\s*=/gi, '');
// Escape special characters
return validator.escape(str);
}
// Export sanitizeString
module.exports.sanitizeString = sanitizeString;
/**
* Validate username
*/
function validateUsername(username) {
const errors = [];
const rules = VALIDATION_RULES.username;
if (!username || typeof username !== 'string') {
errors.push('Username is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = username.trim();
if (trimmed.length < rules.minLength) {
errors.push(`Username must be at least ${rules.minLength} characters`);
}
if (trimmed.length > rules.maxLength) {
errors.push(`Username must not exceed ${rules.maxLength} characters`);
}
if (!rules.pattern.test(trimmed)) {
errors.push('Username can only contain letters, numbers, hyphens, and underscores');
}
const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed;
return {
valid: errors.length === 0,
errors,
sanitized
};
}
/**
* Validate email
*/
function validateEmail(email) {
const errors = [];
const rules = VALIDATION_RULES.email;
if (!email || typeof email !== 'string') {
errors.push('Email is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = email.trim().toLowerCase();
if (!validator.isEmail(trimmed)) {
errors.push('Invalid email format');
}
if (trimmed.length > rules.maxLength) {
errors.push(`Email must not exceed ${rules.maxLength} characters`);
}
const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed;
return {
valid: errors.length === 0,
errors,
sanitized
};
}
/**
* Validate URL
*/
function validateUrl(url, allowLocalhost = false) {
const errors = [];
const rules = VALIDATION_RULES.url;
if (!url || typeof url !== 'string') {
errors.push('URL is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = url.trim();
if (trimmed.length > rules.maxLength) {
errors.push(`URL must not exceed ${rules.maxLength} characters`);
}
// Check if URL is valid and uses allowed protocols
const options = {
protocols: rules.protocols,
require_protocol: true,
allow_underscores: true
};
if (!allowLocalhost) {
options.disallow_auth = false;
}
if (!validator.isURL(trimmed, options)) {
errors.push('Invalid URL format');
}
// Additional security checks
if (trimmed.includes('javascript:')) {
errors.push('URL contains invalid content');
}
return {
valid: errors.length === 0,
errors,
sanitized: trimmed
};
}
/**
* Validate playlist name
*/
function validatePlaylistName(name) {
const errors = [];
const rules = VALIDATION_RULES.playlistName;
if (!name || typeof name !== 'string') {
errors.push('Playlist name is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = name.trim();
if (trimmed.length < rules.minLength) {
errors.push(`Playlist name must be at least ${rules.minLength} character`);
}
if (trimmed.length > rules.maxLength) {
errors.push(`Playlist name must not exceed ${rules.maxLength} characters`);
}
if (!rules.pattern.test(trimmed)) {
errors.push('Playlist name contains invalid characters');
}
const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed;
return {
valid: errors.length === 0,
errors,
sanitized
};
}
/**
* Validate channel name
*/
function validateChannelName(name) {
const errors = [];
const rules = VALIDATION_RULES.channelName;
if (!name || typeof name !== 'string') {
errors.push('Channel name is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = name.trim();
if (trimmed.length < rules.minLength) {
errors.push(`Channel name must be at least ${rules.minLength} character`);
}
if (trimmed.length > rules.maxLength) {
errors.push(`Channel name must not exceed ${rules.maxLength} characters`);
}
const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed;
return {
valid: errors.length === 0,
errors,
sanitized
};
}
/**
* Validate description/text field
*/
function validateDescription(description) {
const errors = [];
const rules = VALIDATION_RULES.description;
if (!description) {
return { valid: true, errors: [], sanitized: '' };
}
if (typeof description !== 'string') {
errors.push('Description must be a string');
return { valid: false, errors, sanitized: null };
}
const trimmed = description.trim();
if (trimmed.length > rules.maxLength) {
errors.push(`Description must not exceed ${rules.maxLength} characters`);
}
const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed;
return {
valid: errors.length === 0,
errors,
sanitized
};
}
/**
* Validate filename
*/
function validateFilename(filename) {
const errors = [];
const rules = VALIDATION_RULES.filename;
if (!filename || typeof filename !== 'string') {
errors.push('Filename is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = filename.trim();
if (trimmed.length > rules.maxLength) {
errors.push(`Filename must not exceed ${rules.maxLength} characters`);
}
if (!rules.pattern.test(trimmed)) {
errors.push('Filename contains invalid characters');
}
// Check for path traversal attempts
if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
errors.push('Filename contains invalid path characters');
}
const sanitized = rules.sanitize ? sanitizeString(trimmed) : trimmed;
return {
valid: errors.length === 0,
errors,
sanitized
};
}
/**
* Validate setting key
*/
function validateSettingKey(key) {
const errors = [];
const rules = VALIDATION_RULES.settingKey;
if (!key || typeof key !== 'string') {
errors.push('Setting key is required');
return { valid: false, errors, sanitized: null };
}
const trimmed = key.trim();
if (trimmed.length > rules.maxLength) {
errors.push(`Setting key must not exceed ${rules.maxLength} characters`);
}
if (!rules.pattern.test(trimmed)) {
errors.push('Setting key contains invalid characters');
}
return {
valid: errors.length === 0,
errors,
sanitized: trimmed
};
}
/**
* Validate integer
*/
function validateInteger(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
const errors = [];
const num = parseInt(value, 10);
if (isNaN(num)) {
errors.push('Must be a valid integer');
return { valid: false, errors, sanitized: null };
}
if (num < min) {
errors.push(`Must be at least ${min}`);
}
if (num > max) {
errors.push(`Must not exceed ${max}`);
}
return {
valid: errors.length === 0,
errors,
sanitized: num
};
}
/**
* Validate boolean
*/
function validateBoolean(value) {
if (typeof value === 'boolean') {
return { valid: true, errors: [], sanitized: value };
}
if (value === 'true' || value === '1' || value === 1) {
return { valid: true, errors: [], sanitized: true };
}
if (value === 'false' || value === '0' || value === 0) {
return { valid: true, errors: [], sanitized: false };
}
return {
valid: false,
errors: ['Must be a valid boolean'],
sanitized: null
};
}
/**
* Validate JSON
*/
function validateJSON(value, maxSize = 10000) {
const errors = [];
if (typeof value === 'object') {
const jsonString = JSON.stringify(value);
if (jsonString.length > maxSize) {
errors.push(`JSON data exceeds maximum size of ${maxSize} characters`);
}
return {
valid: errors.length === 0,
errors,
sanitized: value
};
}
if (typeof value !== 'string') {
errors.push('Must be valid JSON');
return { valid: false, errors, sanitized: null };
}
try {
const parsed = JSON.parse(value);
if (value.length > maxSize) {
errors.push(`JSON data exceeds maximum size of ${maxSize} characters`);
}
return {
valid: errors.length === 0,
errors,
sanitized: parsed
};
} catch (e) {
errors.push('Invalid JSON format');
return { valid: false, errors, sanitized: null };
}
}
/**
* Sanitize object with multiple fields
*/
function sanitizeObject(obj, schema) {
const sanitized = {};
const errors = {};
let hasErrors = false;
for (const [key, validator] of Object.entries(schema)) {
const value = obj[key];
const result = validator(value);
if (!result.valid) {
errors[key] = result.errors;
hasErrors = true;
} else {
sanitized[key] = result.sanitized;
}
}
return {
valid: !hasErrors,
errors,
sanitized
};
}
module.exports = {
validateUsername,
validateEmail,
validateUrl,
validatePlaylistName,
validateChannelName,
validateDescription,
validateFilename,
validateSettingKey,
validateInteger,
validateBoolean,
validateJSON,
sanitizeString,
sanitizeObject,
VALIDATION_RULES
};