Files
easystream-main/f_core/f_classes/class.security.php
SamiAhmed7777 0b7e2d0a5b feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
2025-10-21 00:39:45 -07:00

566 lines
19 KiB
PHP

<?php
/*******************************************************************************************************************
| Software Name : EasyStream
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
| Software Author : (c) Sami Ahmed
|*******************************************************************************************************************
|
|*******************************************************************************************************************
| This source file is subject to the EasyStream Proprietary License Agreement.
|
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|*******************************************************************************************************************
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|*******************************************************************************************************************/
defined('_ISVALID') or header('Location: /error');
/**
* Enhanced Security Class for Input Validation, Sanitization, and CSRF Protection
*/
class VSecurity
{
private static $instance = null;
private $csrfTokens = [];
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Sanitize and validate GET parameters
* @param string $key Parameter name
* @param string $type Expected type (int, string, email, url, alpha, alphanum)
* @param mixed $default Default value if validation fails
* @param array $options Additional validation options
* @return mixed Sanitized value or default
*/
public static function getParam($key, $type = 'string', $default = null, $options = [])
{
if (!isset($_GET[$key])) {
return $default;
}
return self::validateInput($_GET[$key], $type, $default, $options);
}
/**
* Sanitize and validate POST parameters
* @param string $key Parameter name
* @param string $type Expected type
* @param mixed $default Default value if validation fails
* @param array $options Additional validation options
* @return mixed Sanitized value or default
*/
public static function postParam($key, $type = 'string', $default = null, $options = [])
{
if (!isset($_POST[$key])) {
return $default;
}
return self::validateInput($_POST[$key], $type, $default, $options);
}
/**
* Validate and sanitize input based on type
* @param mixed $input Input value
* @param string $type Expected type
* @param mixed $default Default value
* @param array $options Additional options
* @return mixed Sanitized value
*/
public static function validateInput($input, $type, $default = null, $options = [])
{
// Basic sanitization first
if (is_string($input)) {
$input = trim($input);
}
switch ($type) {
case 'int':
case 'integer':
$value = filter_var($input, FILTER_VALIDATE_INT);
if ($value === false) return $default;
// Check min/max if provided
if (isset($options['min']) && $value < $options['min']) return $default;
if (isset($options['max']) && $value > $options['max']) return $default;
return $value;
case 'float':
case 'decimal':
$value = filter_var($input, FILTER_VALIDATE_FLOAT);
return $value !== false ? $value : $default;
case 'email':
$value = filter_var($input, FILTER_VALIDATE_EMAIL);
return $value !== false ? $value : $default;
case 'url':
$value = filter_var($input, FILTER_VALIDATE_URL);
return $value !== false ? $value : $default;
case 'alpha':
$value = preg_replace('/[^a-zA-Z]/', '', $input);
return !empty($value) ? $value : $default;
case 'alphanum':
$value = preg_replace('/[^a-zA-Z0-9]/', '', $input);
return !empty($value) ? $value : $default;
case 'slug':
$value = preg_replace('/[^a-zA-Z0-9\-_]/', '', $input);
return !empty($value) ? $value : $default;
case 'filename':
$value = preg_replace('/[^a-zA-Z0-9\-_\.]/', '', $input);
return !empty($value) ? $value : $default;
case 'boolean':
case 'bool':
return filter_var($input, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) ?? $default;
case 'html':
// Use existing VFilter for HTML content
$filter = new VFilter();
return $filter->sanitize($input);
case 'string':
default:
// Basic string sanitization
$value = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
// Check length if provided
if (isset($options['max_length']) && strlen($value) > $options['max_length']) {
return $default;
}
if (isset($options['min_length']) && strlen($value) < $options['min_length']) {
return $default;
}
return $value;
}
}
/**
* Generate CSRF token
* @param string $action Action name for token scope
* @return string CSRF token
*/
public static function generateCSRFToken($action = 'default')
{
if (!isset($_SESSION)) {
session_start();
}
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_tokens'][$action] = $token;
// Clean old tokens (keep only last 10)
if (isset($_SESSION['csrf_tokens']) && count($_SESSION['csrf_tokens']) > 10) {
$_SESSION['csrf_tokens'] = array_slice($_SESSION['csrf_tokens'], -10, 10, true);
}
return $token;
}
/**
* Validate CSRF token
* @param string $token Token to validate
* @param string $action Action name for token scope
* @return bool True if valid
*/
public static function validateCSRFToken($token, $action = 'default')
{
if (!isset($_SESSION)) {
session_start();
}
if (!isset($_SESSION['csrf_tokens'][$action])) {
return false;
}
$isValid = hash_equals($_SESSION['csrf_tokens'][$action], $token);
// Remove token after use (one-time use)
unset($_SESSION['csrf_tokens'][$action]);
return $isValid;
}
/**
* Get CSRF token HTML input field
* @param string $action Action name
* @return string HTML input field
*/
public static function getCSRFField($action = 'default')
{
$token = self::generateCSRFToken($action);
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token, ENT_QUOTES, 'UTF-8') . '">';
}
/**
* Validate CSRF token from POST data
* @param string $action Action name
* @return bool True if valid
*/
public static function validateCSRFFromPost($action = 'default')
{
$token = $_POST['csrf_token'] ?? '';
return self::validateCSRFToken($token, $action);
}
/**
* Escape output for safe HTML display
* @param string $string String to escape
* @return string Escaped string
*/
public static function escapeOutput($string)
{
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
/**
* Escape for JavaScript output
* @param string $string String to escape
* @return string Escaped string
*/
public static function escapeJS($string)
{
return json_encode($string, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP);
}
/**
* Validate file upload
* @param array $file $_FILES array element
* @param array $allowedTypes Allowed MIME types
* @param int $maxSize Maximum file size in bytes
* @return array Result with 'valid' boolean and 'error' message
*/
public static function validateFileUpload($file, $allowedTypes = [], $maxSize = 10485760)
{
if (!isset($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) {
return ['valid' => false, 'error' => 'No file uploaded or invalid upload'];
}
// Check file size
if ($file['size'] > $maxSize) {
return ['valid' => false, 'error' => 'File too large'];
}
// Check MIME type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!empty($allowedTypes) && !in_array($mimeType, $allowedTypes)) {
return ['valid' => false, 'error' => 'Invalid file type'];
}
return ['valid' => true, 'mime_type' => $mimeType];
}
/**
* Enhanced rate limiting check with Redis support
* @param string $key Unique identifier (IP, user ID, etc.)
* @param int $maxAttempts Maximum attempts
* @param int $timeWindow Time window in seconds
* @param string $action Action being rate limited (for logging)
* @return bool True if within limits
*/
public static function checkRateLimit($key, $maxAttempts = 10, $timeWindow = 300, $action = 'unknown')
{
$now = time();
$rateLimitKey = 'rate_limit_' . $key;
// Try Redis first for better performance and persistence
if (self::useRedisRateLimit($rateLimitKey, $maxAttempts, $timeWindow, $now)) {
return true;
}
// Fallback to session-based rate limiting
if (!isset($_SESSION)) {
session_start();
}
if (!isset($_SESSION[$rateLimitKey])) {
$_SESSION[$rateLimitKey] = [];
}
// Clean old attempts
$_SESSION[$rateLimitKey] = array_filter($_SESSION[$rateLimitKey], function($timestamp) use ($now, $timeWindow) {
return ($now - $timestamp) < $timeWindow;
});
// Check if limit exceeded
if (count($_SESSION[$rateLimitKey]) >= $maxAttempts) {
// Log rate limit violation
$logger = VLogger::getInstance();
$logger->logSecurityEvent("Rate limit exceeded for key: {$key}, action: {$action}", [
'key' => $key,
'action' => $action,
'max_attempts' => $maxAttempts,
'time_window' => $timeWindow,
'current_attempts' => count($_SESSION[$rateLimitKey])
]);
return false;
}
// Add current attempt
$_SESSION[$rateLimitKey][] = $now;
return true;
}
/**
* Redis-based rate limiting
* @param string $key Rate limit key
* @param int $maxAttempts Maximum attempts
* @param int $timeWindow Time window in seconds
* @param int $now Current timestamp
* @return bool|null True if allowed, false if exceeded, null if Redis unavailable
*/
private static function useRedisRateLimit($key, $maxAttempts, $timeWindow, $now)
{
try {
$redis = self::getRedisConnection();
if (!$redis) {
return null;
}
// Use Redis sorted set for sliding window rate limiting
$redisKey = 'rl:' . $key;
// Remove old entries
$redis->zRemRangeByScore($redisKey, 0, $now - $timeWindow);
// Count current attempts
$currentAttempts = $redis->zCard($redisKey);
if ($currentAttempts >= $maxAttempts) {
// Log rate limit violation
$logger = VLogger::getInstance();
$logger->logSecurityEvent("Redis rate limit exceeded for key: {$key}", [
'key' => $key,
'max_attempts' => $maxAttempts,
'time_window' => $timeWindow,
'current_attempts' => $currentAttempts
]);
return false;
}
// Add current attempt
$redis->zAdd($redisKey, $now, uniqid());
$redis->expire($redisKey, $timeWindow);
return true;
} catch (Exception $e) {
// Redis failed, fall back to session-based rate limiting
return null;
}
}
/**
* Get Redis connection
* @return Redis|null
*/
private static function getRedisConnection()
{
static $redis = null;
static $connectionAttempted = false;
if ($connectionAttempted && $redis === null) {
return null;
}
if ($redis !== null) {
return $redis;
}
$connectionAttempted = true;
try {
if (!class_exists('Redis')) {
return null;
}
$redis = new Redis();
$host = getenv('REDIS_HOST') ?: 'redis';
$port = (int)(getenv('REDIS_PORT') ?: 6379);
$db = (int)(getenv('REDIS_DB') ?: 0);
if (!$redis->connect($host, $port, 2)) {
return null;
}
if ($db > 0) {
$redis->select($db);
}
return $redis;
} catch (Exception $e) {
return null;
}
}
/**
* Advanced security monitoring
* @param string $event Security event type
* @param array $context Additional context
*/
public static function logSecurityEvent($event, $context = [])
{
$logger = VLogger::getInstance();
$securityContext = array_merge($context, [
'ip' => self::getClientIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'request_uri' => $_SERVER['REQUEST_URI'] ?? '',
'request_method' => $_SERVER['REQUEST_METHOD'] ?? '',
'user_id' => $_SESSION['USER_ID'] ?? null,
'session_id' => session_id() ?: null,
'timestamp' => date('Y-m-d H:i:s'),
'security_event' => true
]);
$logger->logSecurityEvent($event, $securityContext);
}
/**
* Get client IP address with proxy support
* @return string
*/
private static function getClientIP()
{
$ipKeys = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_X_CLUSTER_CLIENT_IP',
'HTTP_FORWARDED_FOR',
'HTTP_FORWARDED',
'REMOTE_ADDR'
];
foreach ($ipKeys as $key) {
if (array_key_exists($key, $_SERVER) === true) {
$ip = $_SERVER[$key];
if (strpos($ip, ',') !== false) {
$ip = explode(',', $ip)[0];
}
$ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
/**
* Check for suspicious activity patterns
* @param string $userId User ID or IP
* @param string $action Action being performed
* @return bool True if activity seems suspicious
*/
public static function detectSuspiciousActivity($userId, $action)
{
$suspiciousPatterns = [
'rapid_requests' => ['limit' => 100, 'window' => 60],
'failed_logins' => ['limit' => 10, 'window' => 300],
'password_resets' => ['limit' => 5, 'window' => 3600],
'file_uploads' => ['limit' => 50, 'window' => 3600]
];
if (isset($suspiciousPatterns[$action])) {
$pattern = $suspiciousPatterns[$action];
$key = "suspicious_{$action}_{$userId}";
if (!self::checkRateLimit($key, $pattern['limit'], $pattern['window'], $action)) {
self::logSecurityEvent("Suspicious activity detected: {$action}", [
'user_id' => $userId,
'action' => $action,
'pattern' => $pattern
]);
return true;
}
}
return false;
}
/**
* Enhanced file upload validation with security scanning
* @param array $file $_FILES array element
* @param array $allowedTypes Allowed MIME types
* @param int $maxSize Maximum file size in bytes
* @param bool $scanForMalware Enable malware scanning
* @return array Result with 'valid' boolean and 'error' message
*/
public static function validateFileUploadAdvanced($file, $allowedTypes = [], $maxSize = 10485760, $scanForMalware = true)
{
$result = self::validateFileUpload($file, $allowedTypes, $maxSize);
if (!$result['valid']) {
return $result;
}
// Additional security checks
$filename = $file['name'] ?? '';
$tmpName = $file['tmp_name'] ?? '';
// Check for dangerous file extensions
$dangerousExtensions = ['php', 'phtml', 'php3', 'php4', 'php5', 'pht', 'phar', 'exe', 'bat', 'cmd', 'scr', 'vbs', 'js', 'jar'];
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (in_array($extension, $dangerousExtensions)) {
self::logSecurityEvent("Dangerous file upload attempt", [
'filename' => $filename,
'extension' => $extension,
'mime_type' => $result['mime_type']
]);
return ['valid' => false, 'error' => 'File type not allowed for security reasons'];
}
// Check file content for embedded scripts
if ($scanForMalware && is_file($tmpName)) {
$content = file_get_contents($tmpName, false, null, 0, 8192); // Read first 8KB
$maliciousPatterns = [
'/<\?php/i',
'/<script/i',
'/eval\s*\(/i',
'/exec\s*\(/i',
'/system\s*\(/i',
'/shell_exec\s*\(/i',
'/base64_decode\s*\(/i'
];
foreach ($maliciousPatterns as $pattern) {
if (preg_match($pattern, $content)) {
self::logSecurityEvent("Malicious content detected in upload", [
'filename' => $filename,
'pattern' => $pattern,
'mime_type' => $result['mime_type']
]);
return ['valid' => false, 'error' => 'File contains potentially malicious content'];
}
}
}
return $result;
}
}