diff --git a/index-update.php b/index-update.php new file mode 100644 index 0000000..dc3cc82 --- /dev/null +++ b/index-update.php @@ -0,0 +1,804 @@ +', '"', "'"], ['<', '>', '"', '''], $trimmed); +} + +/** + * Generate CSRF token + */ +function generate_csrf_token() { + if (session_status() === PHP_SESSION_NONE) { + // Configure secure session settings + ini_set('session.cookie_httponly', 1); + ini_set('session.cookie_secure', 1); + ini_set('session.cookie_samesite', 'Strict'); + session_start(); + } + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(CSRF_TOKEN_LENGTH)); + } + return $_SESSION['csrf_token']; +} + +/** + * Validate CSRF token + */ +function validate_csrf_token($token) { + if (session_status() === PHP_SESSION_NONE) { + // Configure secure session settings + ini_set('session.cookie_httponly', 1); + ini_set('session.cookie_secure', 1); + ini_set('session.cookie_samesite', 'Strict'); + session_start(); + } + return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); +} + +/** + * Check if user is authenticated + */ +function is_authenticated() { + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true; +} + +/** + * Authenticate user + */ +function authenticate($password) { + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + if ($password === APP_PASSWORD) { + $_SESSION['authenticated'] = true; + return true; + } + return false; +} + +/** + * Logout user + */ +function logout() { + if (session_status() === PHP_SESSION_NONE) { + session_start(); + } + $_SESSION['authenticated'] = false; + session_destroy(); +} + +/** + * Encrypt file content + */ +function encrypt_file($source_path, $destination_path, $password) { + $key = hash('sha256', $password, true); + $iv = openssl_random_pseudo_bytes(16); + + $content = file_get_contents($source_path); + $encrypted = openssl_encrypt($content, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); + + // Store IV + encrypted data + file_put_contents($destination_path, $iv . $encrypted); + + return true; +} + +/** + * Decrypt file content + */ +function decrypt_file($source_path, $password) { + $key = hash('sha256', $password, true); + + $content = file_get_contents($source_path); + $iv = substr($content, 0, 16); + $encrypted = substr($content, 16); + + $decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv); + + return $decrypted; +} + +/** + * Sanitize filename for headers to prevent HTTP Response Splitting + */ +function sanitize_filename_for_header($filename) { + // Remove any control characters and limit to ASCII printable chars + $filename = preg_replace('/[\x00-\x1F\x7F-\xFF]/', '', $filename); + // Remove quotes and backslashes to prevent header injection + $filename = str_replace(['"', '\\', "\r", "\n"], '', $filename); + // Limit length to prevent excessively long headers + return mb_substr($filename, 0, 255); +} + +/** + * Generate a UUID v4 + */ +function generate_uuid() { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); +} + +/** + * Get client IP address + */ +function get_client_ip() { + $ip_keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'REMOTE_ADDR']; + + foreach ($ip_keys as $key) { + if (!empty($_SERVER[$key])) { + $ip = $_SERVER[$key]; + // Handle comma-separated list of IPs + if (strpos($ip, ',') !== false) { + $ip = trim(explode(',', $ip)[0]); + } + // Validate 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'; +} + +/** + * Hash IP for GDPR compliance + */ +function hash_ip($ip) { + // normalize IPv4/IPv6 + $normalized = inet_ntop(inet_pton($ip)); + return hash('sha256', HASH_SALT . $normalized); +} + +/** + * Check if IP is rate limited for downloads + */ +function is_rate_limited($ip) { + if (!file_exists(DOWNLOAD_LOG)) { + return false; + } + + $hashed_ip = hash_ip($ip); + $current_time = time(); + + $fp = fopen(DOWNLOAD_LOG, 'r'); + if (!$fp) return false; + + while (($line = fgets($fp)) !== false) { + $parts = explode('|', trim($line)); + if (count($parts) >= 2) { + [$log_ip, $timestamp] = $parts; + $timestamp = intval($timestamp); + + if ($log_ip === $hashed_ip && ($current_time - $timestamp) < DOWNLOAD_RATE_LIMIT_SECONDS) { + fclose($fp); + return true; + } + } + } + + fclose($fp); + return false; +} + +/** + * Log download attempt + */ +function log_download($ip) { + $hashed_ip = hash_ip($ip); + $log_entry = $hashed_ip . '|' . time() . "\n"; + + // Append safely + file_put_contents(DOWNLOAD_LOG, $log_entry, FILE_APPEND | LOCK_EX); + + // Keep log size under control + $lines = file(DOWNLOAD_LOG, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (count($lines) > MAX_LOG_LINES) { + $lines = array_slice($lines, -MAX_LOG_LINES); // keep recent only + file_put_contents(DOWNLOAD_LOG, implode("\n", $lines) . "\n", LOCK_EX); + } +} + +/** + * Load files data from JSON + */ +function load_files_data() { + $json_content = file_get_contents(FILES_JSON); + return json_decode($json_content, true) ?: []; +} + +/** + * Save files data to JSON + */ +function save_files_data($data) { + return file_put_contents(FILES_JSON, json_encode($data, JSON_PRETTY_PRINT), LOCK_EX); +} + +/** + * Validate file extension + */ +function is_allowed_file_type($filename) { + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + return in_array($extension, ALLOWED_EXTENSIONS); +} + +/** + * Clean up old files + */ +function cleanup_old_files() { + $files_data = load_files_data(); + $current_time = time(); + $deleted = false; + + foreach ($files_data as $uuid => $file_info) { + $age_days = ($current_time - $file_info['upload_timestamp']) / (60 * 60 * 24); + + if ($age_days > AUTO_DELETE_DAYS) { + $file_path = DOWNLOAD_DIR . '/' . $uuid; + if (file_exists($file_path)) { + unlink($file_path); + } + unset($files_data[$uuid]); + $deleted = true; + } + } + + if ($deleted) { + save_files_data($files_data); + } +} + +/** + * Display error message and exit + */ +function display_error($message) { + error_log("User error: " . $message); + echo render_page("Error", "
$message
"); + exit; +} + +/** + * Display success message and exit + */ +function display_success($message) { + echo render_page("Success", "
$message
"); + exit; +} + +/** + * Render HTML page + */ +function render_page($title, $content) { + // Set security headers + header('Content-Type: text/html; charset=UTF-8'); + header('X-Content-Type-Options: nosniff'); + header('X-Frame-Options: DENY'); + header('X-XSS-Protection: 1; mode=block'); + header('Content-Security-Policy: default-src \'self\' cdn.tailwindcss.com; script-src \'self\' \'unsafe-inline\' cdn.tailwindcss.com; style-src \'self\' \'unsafe-inline\' cdn.tailwindcss.com; img-src \'self\' data:; connect-src \'self\''); + + $base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . dirname($_SERVER['SCRIPT_NAME']); + + $logout_button = ''; + if (is_authenticated() && !isset($_GET['download'])) { + $logout_button = 'Se déconnecter'; + } + + return << + + + + + $title - HopTransfert + + + +
+
+

HopTransfert

+ $logout_button +
+ $content +
+ + +HTML; +} + +// ============================================================================= +// MAIN APPLICATION LOGIC +// ============================================================================= + +// Force UTF-8 encoding +mb_internal_encoding('UTF-8'); +mb_http_output('UTF-8'); + +// Cleanup old files +cleanup_old_files(); + +// Sanitize all input +$_GET = sanitize_input($_GET); +$_POST = sanitize_input($_POST); + +// Handle logout +if (isset($_GET['logout'])) { + logout(); + header('Location: ' . $_SERVER['SCRIPT_NAME']); + exit; +} + +// Check authentication for upload page +if (!isset($_GET['download'])) { + if (!is_authenticated()) { + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['app_password'])) { + if (authenticate($_POST['app_password'])) { + header('Location: ' . $_SERVER['SCRIPT_NAME']); + exit; + } else { + show_login_form('Mot de passe incorrect'); + exit; + } + } else { + show_login_form(); + exit; + } + } +} + +// Route handling +if (isset($_GET['download'])) { + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) { + handle_download($_GET['download'], $_POST['password'], $_POST['csrf_token'] ?? ''); + } else { + show_download_form($_GET['download']); + } +} elseif ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) { + handle_upload(); +} else { + show_upload_form(); +} + +// ============================================================================= +// ROUTE HANDLERS +// ============================================================================= + +/** + * Show login form + */ +function show_login_form($error = '') { + $error_html = $error ? "
$error
" : ''; + + $form = " + $error_html +
+
+ + +
+ + +
+ "; + + echo render_page("Connexion", $form); +} + +/** + * Handle file upload + */ +function handle_upload() { + try { + // Validate request method + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + throw new Exception('Invalid request method'); + } + + // Validate CSRF token + $csrf_token = $_POST['csrf_token'] ?? ''; + if (!validate_csrf_token($csrf_token)) { + throw new Exception('Invalid CSRF token. Please refresh the page and try again.'); + } + + // Check if file was uploaded + if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { + throw new Exception('No file uploaded or upload error occurred'); + } + + $file = $_FILES['file']; + $password = $_POST['password'] ?? ''; + + // Validate password + if (strlen($password) < PASSWORD_MIN_LENGTH) { + throw new Exception('Password must be at least ' . PASSWORD_MIN_LENGTH . ' characters long'); + } + + // Validate file size + if ($file['size'] > MAX_FILE_SIZE) { + throw new Exception('File size exceeds maximum allowed size of ' . (MAX_FILE_SIZE / 1024 / 1024) . 'MB'); + } + + // Validate file type + if (!is_allowed_file_type($file['name'])) { + throw new Exception('File type not allowed. Allowed types: ' . implode(', ', ALLOWED_EXTENSIONS)); + } + + // Generate UUID and hash password + $uuid = generate_uuid(); + $password_hash = password_hash($password, PASSWORD_DEFAULT); + + // Encrypt and save file + $file_path = DOWNLOAD_DIR . '/' . $uuid; + if (!encrypt_file($file['tmp_name'], $file_path, $password)) { + throw new Exception('Failed to encrypt and save file'); + } + + // Add to files database + $files_data = load_files_data(); + $files_data[$uuid] = [ + 'uuid' => $uuid, + 'original_filename' => $file['name'], + 'download_password_hash' => $password_hash, + 'upload_timestamp' => time() + ]; + + if (!save_files_data($files_data)) { + // Clean up uploaded file if database save fails + unlink($file_path); + throw new Exception('Failed to save file metadata'); + } + + // Generate links + $base_url = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME']; + $download_link = $base_url . '?download=' . urlencode($uuid); + + $success_message = " +

Fichier uploadé avec succès !

+
+
+ +
+ + +
+
+
+

Important : Partagez ce lien avec le destinataire. Il aura besoin du mot de passe que vous avez défini.

+

Le fichier sera automatiquement supprimé après téléchargement ou après " . AUTO_DELETE_DAYS . " jours.

+

Les téléchargements sont limités à 1 par minute par adresse IP.

+
+ Uploader un autre fichier +
+ + + "; + + display_success($success_message); + + } catch (Exception $e) { + display_error($e->getMessage()); + } +} + +/** + * Show download form + */ +function show_download_form($uuid) { + // Validate UUID format + if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid)) { + display_error('Invalid file ID'); + return; + } + + // Load files data to check if file exists + $files_data = load_files_data(); + if (!isset($files_data[$uuid])) { + display_error('File not found or has been deleted'); + return; + } + + $file_info = $files_data[$uuid]; + $original_filename = htmlspecialchars($file_info['original_filename']); + $csrf_token = generate_csrf_token(); + + $form = " +
+

Télécharger le fichier

+

Fichier : $original_filename

+
+ +
+ +
+ + +

Entrez le mot de passe fourni par l'expéditeur.

+
+ + +
+ +
+

Note : Le fichier sera automatiquement supprimé après téléchargement.

+

Les téléchargements sont limités à 1 par minute par adresse IP.

+
+ "; + + echo render_page("Télécharger", $form); +} + +/** + * Handle file download + */ +function handle_download($uuid, $token, $csrf_token) { + try { + // Validate CSRF token + if (!validate_csrf_token($csrf_token)) { + throw new Exception('Invalid CSRF token. Please refresh the page and try again.'); + } + + // Validate UUID format + if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid)) { + throw new Exception('Invalid file ID'); + } + + // Check rate limiting + $client_ip = get_client_ip(); + if (is_rate_limited($client_ip)) { + throw new Exception('Rate limit exceeded. Please wait before downloading another file.'); + } + + // Load files data + $files_data = load_files_data(); + + // Check if file exists in database + if (!isset($files_data[$uuid])) { + throw new Exception('File not found or has been deleted'); + } + + $file_info = $files_data[$uuid]; + + // Verify password + if (!password_verify($token, $file_info['download_password_hash'])) { + throw new Exception('Invalid download password'); + } + + $file_path = DOWNLOAD_DIR . '/' . $uuid; + + // Check if physical file exists + if (!file_exists($file_path)) { + // Remove orphaned database entry + unset($files_data[$uuid]); + save_files_data($files_data); + throw new Exception('File not found or has been deleted'); + } + + // Decrypt file + $decrypted_content = decrypt_file($file_path, $token); + + if ($decrypted_content === false) { + throw new Exception('Failed to decrypt file. Invalid password.'); + } + + // Log the download + log_download($client_ip); + + // Serve the file + $original_filename = sanitize_filename_for_header($file_info['original_filename']); + + // Set headers for file download + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $original_filename . '"'); + header('Content-Length: ' . strlen($decrypted_content)); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + + // Security headers + header('X-Content-Type-Options: nosniff'); + header('X-Frame-Options: DENY'); + header('X-XSS-Protection: 1; mode=block'); + + // Output file contents + echo $decrypted_content; + + // Delete file after successful download + unlink($file_path); + unset($files_data[$uuid]); + save_files_data($files_data); + + exit; + + } catch (Exception $e) { + display_error($e->getMessage()); + } +} + +/** + * Show upload form + */ +function show_upload_form() { + $max_size_mb = MAX_FILE_SIZE / 1024 / 1024; + $allowed_types = implode(', ', ALLOWED_EXTENSIONS); + + $csrf_token = generate_csrf_token(); + + $form = " +
+ + +
+ + +

Taille max : {$max_size_mb}MB. Types autorisés : $allowed_types

+
+ +
+ + +

Minimum " . PASSWORD_MIN_LENGTH . " caractères. Ce mot de passe sera requis pour télécharger le fichier.

+
+ + + + +
+ +
+

Comment ça marche :

+ +
+ + + "; + + echo render_page("Upload", $form); +} + +?>