diff --git a/class/comment.php b/class/comment.php index 3b00f29..307bbf9 100644 --- a/class/comment.php +++ b/class/comment.php @@ -236,7 +236,32 @@ function _commentSettingsGet($param = '') // Возвращаем количество комментариев для запрашиваемого документа return $comments[$document_id]; } +/** + * Логирование подозрительной активности при загрузке файлов + */ + private function _logUploadSecurity($status, $file_info, $context = []) + { + $log_dir = BASE_DIR . '/modules/comment/logs/'; + if (!is_dir($log_dir)) @mkdir($log_dir, 0775, true); + $log_file = $log_dir . 'security_upload.log'; + $date = date('Y-m-d H:i:s'); + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + $user_id = $_SESSION['user_id'] ?? 0; + $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'none'; + + // Формируем строку контекста: ID сообщения, имя автора и т.д. + $ctx_info = ""; + if (!empty($context)) { + foreach ($context as $key => $val) { + $ctx_info .= " | $key: $val"; + } + } + + $message = "[$date] IP: $ip | UID: $user_id{$ctx_info} | STATUS: $status | FILE: {$file_info['name']} | MIME: {$file_info['mime']} | SIZE: {$file_info['size']} bytes | UA: $user_agent\n"; + + @file_put_contents($log_file, $message, FILE_APPEND); + } /** * Следующие методы описывают работу модуля в Публичной части сайта. @@ -659,17 +684,34 @@ function commentPostFormShow($tpl_dir) $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); -// Проверки расширения и безопасности -$is_allowed_ext = in_array($ext, $allowed_extensions); -$is_dangerous = in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi']); +// Определяем списки расширений для разной логики проверок +$img_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']; +$is_image_ext = in_array($ext, $img_exts); -// Условие: расширение разрешено, файл не опасен и проходит по размеру -if ($is_allowed_ext && !$is_dangerous && $file['size'] > 0 && $file['size'] <= $max_file_size_bytes) +// 1. Проверки расширения и базовой безопасности +$is_allowed_ext = in_array($ext, $allowed_extensions); +$is_dangerous = in_array($ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi', 'html', 'js']); + +// 2. Проверка на "лже-картинку" (только если расширение из списка изображений) +$mime_is_valid = true; +if ($is_image_ext && strpos($mime, 'image/') !== 0) { + $mime_is_valid = false; // Попытка просунуть скрипт под видом картинки +} + +// 3. Защита от опасных MIME-типов (даже если админ разрешил такое расширение) +$is_dangerous_mime = in_array($mime, [ + 'text/php', 'text/x-php', 'application/x-php', + 'application/x-httpd-php', 'application/x-executable', + 'text/html', 'text/javascript' +]); + +// Итоговое условие: разрешено в админке + не опасно + валидный MIME + размер +if ($is_allowed_ext && !$is_dangerous && !$is_dangerous_mime && $mime_is_valid && $file['size'] > 0 && $file['size'] <= $max_file_size_bytes) { // 1. Берем имя $original_basename = pathinfo($file['name'], PATHINFO_FILENAME); - // 2. Очищаем (БЕЗ $this->, просто вызываем функцию) + // 2. Очищаем $clean_name = prepare_fname($original_basename); // 3. Если пусто - дефолт @@ -685,6 +727,39 @@ if ($is_allowed_ext && !$is_dangerous && $file['size'] > 0 && $file['size'] <= $ $processed_hashes[] = $file_hash; } } +else + { + // 1. Формируем причину отказа + $reason = "ОТКАЗ ПРИ СОЗДАНИИ КОММЕНТАРИЯ:"; + if (!$is_allowed_ext) $reason .= " Запрещенное расширение($ext);"; + if ($is_dangerous) $reason .= " Опасный формат файла;"; + if ($is_dangerous_mime) $reason .= " Вредоносный тип контента($mime);"; + if (!$mime_is_valid) $reason .= " Обнаружена подмена (Fake Image);"; + if ($file['size'] > $max_file_size_bytes) $reason .= " Файл слишком большой;"; + if ($file['size'] <= 0) $reason .= " Пустой файл;"; + + // 2. Собираем контекст (кто, куда и под каким именем писал) + // Берём данные из $_POST, так как запись в БД ещё не создана + $context = [ + 'AUTHOR' => $_POST['comment_author_name'] ?? 'Неизвестно', + 'EMAIL' => $_POST['comment_author_email'] ?? '-', + 'DOC_ID' => (int)($_POST['doc_id'] ?? 0) + ]; + + // 3. Записываем в лог с контекстом + $this->_logUploadSecurity($reason, [ + 'name' => $file['name'], + 'mime' => $mime, + 'size' => $file['size'] + ], $context); + + // 4. Защита от дублей в логе (если один и тот же файл пришел дважды) + $processed_hashes[] = $file_hash; + } + + + + } } @@ -877,6 +952,8 @@ if (!$can_upload && isset($_FILES['comment_image'])) { // Получаем текущие файлы из БД в массив $current_files = !empty($row['comment_file']) ? explode(',', $row['comment_file']) : []; + $processed_hashes = []; // Для защиты лога от дубликатов + // --- Обработка выборочного удаления --- if (!empty($_POST['delete_files'])) { $files_to_remove = explode(',', $_POST['delete_files']); @@ -919,46 +996,81 @@ if (!$can_upload && isset($_FILES['comment_image'])) { // Перебираем загруженные файлы foreach ($_FILES['comment_image']['name'] as $i => $fname) { - if ($_FILES['comment_image']['error'][$i] == UPLOAD_ERR_OK) { - - $tmp_name = $_FILES['comment_image']['tmp_name'][$i]; - $file_ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION)); - $file_size = $_FILES['comment_image']['size'][$i]; - // Проверка MIME-типа +if ($_FILES['comment_image']['error'][$i] == UPLOAD_ERR_OK) { + + $tmp_name = $_FILES['comment_image']['tmp_name'][$i]; + $file_size = $_FILES['comment_image']['size'][$i]; + $file_ext = strtolower(pathinfo($fname, PATHINFO_EXTENSION)); + + // 1. Проверка MIME-типа и хэша $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $tmp_name); finfo_close($finfo); - // Проверки расширения и опасных файлов - $is_allowed_ext = in_array($file_ext, $allowed_extensions); - $is_dangerous = in_array($file_ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi']); + $file_hash = md5_file($tmp_name); + if (in_array($file_hash, $processed_hashes)) continue; - if ($is_allowed_ext && !$is_dangerous && $file_size > 0 && $file_size <= ($max_kb * 1024)) + // 2. Логика безопасности + $img_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']; + $is_image_ext = in_array($file_ext, $img_exts); + $is_allowed_ext = in_array($file_ext, $allowed_extensions); + $is_dangerous = in_array($file_ext, ['php', 'php3', 'php4', 'php5', 'phtml', 'exe', 'pl', 'cgi', 'html', 'js']); + + $mime_is_valid = true; + if ($is_image_ext && strpos($mime, 'image/') !== 0) { + $mime_is_valid = false; + } + + $is_dangerous_mime = in_array($mime, [ + 'text/php', 'text/x-php', 'application/x-php', + 'application/x-httpd-php', 'application/x-executable', + 'text/html', 'text/javascript' + ]); + + // 3. Условие загрузки + if ($is_allowed_ext && !$is_dangerous && !$is_dangerous_mime && $mime_is_valid && $file_size > 0 && $file_size <= ($max_kb * 1024)) { if (!is_dir($upload_dir)) @mkdir($upload_dir, 0775, true); + $original_basename = pathinfo($fname, PATHINFO_FILENAME); + $clean_name = prepare_fname($original_basename); + if (empty($clean_name)) $clean_name = 'file'; - // Получаем оригинальное имя файла без расширения - $original_basename = pathinfo($fname, PATHINFO_FILENAME); - - // Очищаем имя - $clean_name = prepare_fname($original_basename); - - // Защита от пустого имени - if (empty($clean_name)) { - $clean_name = 'file'; - } - - // Формируем новое имя: чистое_имя + время + расширение - $new_file_name = $clean_name . '_' . time() . '.' . $file_ext; - + $new_file_name = $clean_name . '_' . time() . '.' . $file_ext; + if (move_uploaded_file($tmp_name, $upload_dir . $new_file_name)) { $current_files[] = $new_file_name; + $processed_hashes[] = $file_hash; } } + else + { + // 4. ЛОГИРОВАНИЕ (с контекстом комментария) + $reason = "ОТКАЗ ПРИ РЕДАКТИРОВАНИИ КОММЕНТАРИЯ:"; + if (!$is_allowed_ext) $reason .= " Запрещенное расширение($file_ext);"; + if ($is_dangerous) $reason .= " Опасный формат файла;"; + if ($is_dangerous_mime) $reason .= " Вредоносный тип контента($mime);"; + if (!$mime_is_valid) $reason .= " Обнаружена подмена (Fake Image);"; + if ($file_size > ($max_kb * 1024)) $reason .= " Файл слишком большой;"; + + $context = [ + 'COMMENT_ID' => $comment_id, + 'AUTHOR_DB' => $row['comment_author_name'], + 'CURRENT_UID' => $user_id + ]; + + $this->_logUploadSecurity($reason, [ + 'name' => $fname, + 'mime' => $mime, + 'size' => $file_size + ], $context); + + $processed_hashes[] = $file_hash; + } } } + } // Собираем итоговую строку файлов для БД через запятую diff --git a/logs/.htaccess b/logs/.htaccess new file mode 100644 index 0000000..9b65eff --- /dev/null +++ b/logs/.htaccess @@ -0,0 +1,6 @@ +# Запрещаем доступ к файлам логов извне (через браузер) +Order Deny,Allow +Deny from all + +# Отключаем листинг директории +Options -Indexes \ No newline at end of file