Initial commit: StreamFlow IPTV platform
This commit is contained in:
commit
73a8ae9ffd
1240 changed files with 278451 additions and 0 deletions
486
backend/utils/inputValidator.js
Normal file
486
backend/utils/inputValidator.js
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
/**
|
||||
* 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue