From 9d427f4870c12c8f9b2f6543cdec3a1798f8c530 Mon Sep 17 00:00:00 2001 From: Repellent Date: Sat, 11 Oct 2025 17:17:12 +0500 Subject: [PATCH] SMTP password security fix --- class/class.settings.php | 293 ++++++++++++++++++++++++-------------- functions/func.common.php | 191 ++++++++++++++++++++++--- functions/func.mail.php | 22 +-- inc/.htaccess | 5 + 4 files changed, 378 insertions(+), 133 deletions(-) create mode 100644 inc/.htaccess diff --git a/class/class.settings.php b/class/class.settings.php index ca90838..8400b5a 100644 --- a/class/class.settings.php +++ b/class/class.settings.php @@ -34,30 +34,47 @@ * Метод отображения настроек * */ - function settingsShow() - { - global $AVE_Template; +function settingsShow() +{ + global $AVE_Template; - $date_formats = array( - '%d.%m.%Y', - '%d %B %Y', - '%A, %d.%m.%Y', - '%A, %d %B %Y' - ); + $date_formats = array( + '%d.%m.%Y', + '%d %B %Y', + '%A, %d.%m.%Y', + '%A, %d %B %Y' + ); - $time_formats = array( - '%d.%m.%Y, %H:%M', - '%d %B %Y, %H:%M', - '%A, %d.%m.%Y (%H:%M)', - '%A, %d %B %Y (%H:%M)' - ); + $time_formats = array( + '%d.%m.%Y, %H:%M', + '%d %B %Y, %H:%M', + '%A, %d.%m.%Y (%H:%M)', + '%A, %d %B %Y (%H:%M)' + ); - $AVE_Template->assign('date_formats', $date_formats); - $AVE_Template->assign('time_formats', $time_formats); - $AVE_Template->assign('row', get_settings()); - $AVE_Template->assign('available_countries', get_country_list(1)); - $AVE_Template->assign('content', $AVE_Template->fetch('settings/settings_main.tpl')); - } + // 1. Получаем настройки (ПАРОЛЬ УЖЕ ДЕШИФРОВАН в кэше get_settings()) + $settings_data_array = get_settings(); + + // 2. Извлекаем строку настроек (обычно это $settings[0]) + $row = (isset($settings_data_array[0]) && is_array($settings_data_array[0])) + ? $settings_data_array[0] + : (is_array($settings_data_array) ? $settings_data_array : array()); + + // 3. КРИТИЧЕСКАЯ ЛОГИКА МАСКИРОВКИ: Заменяем дешифрованный пароль на '****' для отображения + if (isset($row['mail_type']) && $row['mail_type'] === 'smtp' && !empty($row['mail_smtp_pass'])) { + // Дешифрованный пароль заменяется на '****' для поля формы + $row['mail_smtp_pass'] = '****'; + } + + $AVE_Template->assign('date_formats', $date_formats); + $AVE_Template->assign('time_formats', $time_formats); + + // 4. Передаем модифицированный массив с '****' в шаблон + $AVE_Template->assign('row', $row); + + $AVE_Template->assign('available_countries', get_country_list(1)); + $AVE_Template->assign('content', $AVE_Template->fetch('settings/settings_main.tpl')); +} /** * Записывает события переключения файлов в специальный лог. @@ -342,98 +359,162 @@ function settingsCase() * Метод записи настроек * */ - function settingsSave() - { - global $AVE_DB, $AVE_Template; +function settingsSave() +{ + global $AVE_DB, $AVE_Template; + + // ДОБАВЛЕНО: Принудительно загружаем ключ шифрования. + get_smtp_encryption_key(); - $muname = ($_REQUEST['mail_smtp_login']) ? "mail_smtp_login = '" . $_REQUEST['mail_smtp_login'] . "'," : ''; - $mpass = ($_REQUEST['mail_smtp_pass']) ? "mail_smtp_pass = '" . $_REQUEST['mail_smtp_pass'] . "'," : ''; - $msmp = ($_REQUEST['mail_sendmail_path']) ? "mail_sendmail_path = '" . $_REQUEST['mail_sendmail_path'] . "'," : ''; - $mn = ($_REQUEST['mail_from_name']) ? "mail_from_name = '" . $_REQUEST['mail_from_name'] . "'," : ''; - $ma = ($_REQUEST['mail_from']) ? "mail_from = '" . $_REQUEST['mail_from'] . "'," : ''; - $ep = ($_REQUEST['page_not_found_id']) ? "page_not_found_id = '" . $_REQUEST['page_not_found_id'] . "'," : ''; - $sn = ($_REQUEST['site_name']) ? "site_name = '" . $_REQUEST['site_name'] . "'," : ''; - $mp = ($_REQUEST['mail_port']) ? "mail_port = '" . $_REQUEST['mail_port'] . "'," : ''; - $mh = ($_REQUEST['mail_host']) ? "mail_host = '" . $_REQUEST['mail_host'] . "'," : ''; + // ---------------------------------------------------- + // 1. ЛОГИКА ГЕНЕРАЦИИ УНИКАЛЬНОГО КЛЮЧА + // ---------------------------------------------------- + if (isset($_REQUEST['mail_type']) && $_REQUEST['mail_type'] === 'smtp' && !file_exists(BASE_DIR . '/inc/smtp_key.php')) { + + $new_key = create_smtp_key_file(); + + if (empty($new_key)) { + $message = 'Ошибка: Не удалось создать файл ключа для SMTP-пароля. Проверьте права записи в папке inc/.'; + $header = $AVE_Template->get_config_vars('SETTINGS_ERROR'); + $theme = 'error'; - $sql = $AVE_DB->Query(" - UPDATE - " . PREFIX . "_settings - SET - " . $muname . " - " . $mpass . " - mail_smtp_encrypt = '" . $_REQUEST['mail_smtp_encrypt'] . "', - " . $msmp . " - " . $ma . " - " . $mn . " - " . $ep . " - " . $sn . " - " . $mp . " - " . $mh . " - default_country = '" . $_REQUEST['default_country'] . "', - mail_type = '" . $_REQUEST['mail_type'] . "', - mail_content_type = '" . $_REQUEST['mail_content_type'] . "', - mail_word_wrap = '" . (int)$_REQUEST['mail_word_wrap'] . "', - mail_new_user = '" . $_REQUEST['mail_new_user'] . "', - mail_signature = '" . $_REQUEST['mail_signature'] . "', - message_forbidden = '" . $_REQUEST['message_forbidden'] . "', - hidden_text = '" . $_REQUEST['hidden_text'] . "', - navi_box = '" . $_REQUEST['navi_box'] . "', - start_label = '" . $_REQUEST['start_label'] . "', - end_label = '" . $_REQUEST['end_label'] . "', - separator_label = '" . $_REQUEST['separator_label'] . "', - next_label = '" . $_REQUEST['next_label'] . "', - prev_label = '" . $_REQUEST['prev_label'] . "', - total_label = '" . $_REQUEST['total_label'] . "', - link_box = '" . $_REQUEST['link_box'] . "', - total_box = '" . $_REQUEST['total_box'] . "', - active_box = '" . $_REQUEST['active_box'] . "', - separator_box = '" . $_REQUEST['separator_box'] . "', - bread_box = '" . $_REQUEST['bread_box'] . "', - bread_show_main = '" . ($_REQUEST['bread_show_main'] != 0 ? 1 : 0) . "', - bread_show_host = '" . ($_REQUEST['bread_show_host'] != 0 ? 1 : 0) . "', - bread_sepparator = '" . $_REQUEST['bread_sepparator'] . "', - bread_sepparator_use = '" . ($_REQUEST['bread_sepparator_use'] != 0 ? 1 : 0) . "', - bread_link_box = '" . $_REQUEST['bread_link_box'] . "', - bread_link_template = '" . $_REQUEST['bread_link_template'] . "', - bread_self_box = '" . $_REQUEST['bread_self_box'] . "', - bread_link_box_last = '" . ($_REQUEST['bread_link_box_last'] != 0 ? 1 : 0) . "', - date_format = '" . $_REQUEST['date_format'] . "', - time_format = '" . $_REQUEST['time_format'] . "', - use_doctime = '" . intval($_REQUEST['use_doctime']) . "' - WHERE - Id = 1 - "); + if (isset($_REQUEST['ajax']) && $_REQUEST['ajax'] == '1') { + echo json_encode(array('message' => $message, 'header' => $header, 'theme' => $theme)); + } else { + $AVE_Template->assign('message', $message); + header('Location:index.php?do=settings&cp=' . SESSION); + } + exit; + } + } + + // ---------------------------------------------------- + // 2. ОБРАБОТКА И ШИФРОВАНИЕ SMTP ПАРОЛЯ (С ИГНОРОМ ****) + // ---------------------------------------------------- + $smtp_pass_encrypted = null; // Используем null, чтобы пропустить поле в SQL, если оно пустое или **** - if ($sql->_result === false) - { - $message = $AVE_Template->get_config_vars('SETTINGS_SAVED_ERR'); - $header = $AVE_Template->get_config_vars('SETTINGS_ERROR'); - $theme = 'error'; - } - else - { - $this->clearSettingsCache(); + if (isset($_REQUEST['mail_smtp_pass'])) { + + $new_smtp_pass_raw = trim($_REQUEST['mail_smtp_pass']); - $message = $AVE_Template->get_config_vars('SETTINGS_SAVED'); - $header = $AVE_Template->get_config_vars('SETTINGS_SUCCESS'); - $theme = 'accept'; - reportLog($AVE_Template->get_config_vars('SETTINGS_SAVE_MAIN')); - } + // ИСПРАВЛЕНО: Если пароль не пуст И не равен маске + if (!empty($new_smtp_pass_raw) && $new_smtp_pass_raw !== '****') { + + // ПАРОЛЬ БЫЛ ВВЕДЕН ИЛИ ИЗМЕНЕН: Шифруем новый пароль + if (isset($_REQUEST['mail_type']) && $_REQUEST['mail_type'] === 'smtp') { + $smtp_pass_encrypted = encrypt_smtp_pass($new_smtp_pass_raw); + } else { + $smtp_pass_encrypted = $new_smtp_pass_raw; + } + } + // Если $new_smtp_pass_raw === '****' ИЛИ ПУСТОЙ, поле mail_smtp_pass не будет добавлено в SQL. + } - if (isset($_REQUEST['ajax']) && $_REQUEST['ajax'] = '1') - { - echo json_encode(array('message' => $message, 'header' => $header, 'theme' => $theme)); - } - else - { - $AVE_Template->assign('message', $message); - header('Location:index.php?do=settings&cp=' . SESSION); - } - exit; - } + // ---------------------------------------------------- + // 3. ФОРМИРОВАНИЕ УСЛОВНЫХ И ОБЯЗАТЕЛЬНЫХ ПОЛЕЙ + // ---------------------------------------------------- + + $mandatory_fields = array( + "mail_smtp_login" => addslashes($_REQUEST['mail_smtp_login']), + "mail_smtp_encrypt" => addslashes($_REQUEST['mail_smtp_encrypt']), + "mail_sendmail_path" => addslashes($_REQUEST['mail_sendmail_path']), + "mail_from_name" => addslashes($_REQUEST['mail_from_name']), + "mail_from" => addslashes($_REQUEST['mail_from']), + "page_not_found_id" => addslashes($_REQUEST['page_not_found_id']), + "mail_port" => addslashes($_REQUEST['mail_port']), + "mail_host" => addslashes($_REQUEST['mail_host']), + "default_country" => addslashes($_REQUEST['default_country']), + "mail_type" => addslashes($_REQUEST['mail_type']), + "mail_content_type" => addslashes($_REQUEST['mail_content_type']), + "mail_word_wrap" => (int)$_REQUEST['mail_word_wrap'], + "mail_new_user" => addslashes($_REQUEST['mail_new_user']), + "mail_signature" => addslashes($_REQUEST['mail_signature']), + "message_forbidden" => addslashes($_REQUEST['message_forbidden']), + "hidden_text" => addslashes($_REQUEST['hidden_text']), + "date_format" => addslashes($_REQUEST['date_format']), + "time_format" => addslashes($_REQUEST['time_format']), + "use_doctime" => intval($_REQUEST['use_doctime']) + ); + + // ДОБАВЛЯЕМ ПАРОЛЬ ТОЛЬКО ЕСЛИ ОН БЫЛ ВВЕДЕН ИЛИ ИЗМЕНЕН + if ($smtp_pass_encrypted !== null) { + $mandatory_fields["mail_smtp_pass"] = addslashes($smtp_pass_encrypted); + } + + // Поля, которые обновляются ТОЛЬКО, если они были отправлены формой + $conditional_keys = array( + 'site_name', 'navi_box', 'start_label', 'end_label', 'separator_label', + 'next_label', 'prev_label', 'total_label', 'link_box', 'total_box', + 'active_box', 'separator_box', 'bread_box', 'bread_show_main', 'bread_show_host', + 'bread_sepparator', 'bread_sepparator_use', 'bread_link_box', 'bread_link_template', + 'bread_self_box', 'bread_link_box_last' + ); + + $set_clauses = array(); + + // 1. Формируем обязательные поля + foreach ($mandatory_fields as $key => $value) { + $set_clauses[] = "{$key} = '{$value}'"; + } + + // 2. Формируем условные поля + foreach ($conditional_keys as $key) { + if (isset($_REQUEST[$key])) { + $value = $_REQUEST[$key]; + + if (strpos($key, 'bread_') === 0 && (strpos($key, 'show') !== false || strpos($key, 'use') !== false || strpos($key, 'last') !== false)) { + $set_clauses[] = "{$key} = '" . ($value != 0 ? 1 : 0) . "'"; + } else { + $set_clauses[] = "{$key} = '" . addslashes($value) . "'"; + } + } + } + + // Объединяем все части через запятую для SQL + $set_string = implode(",\r\n", $set_clauses); + + // ---------------------------------------------------- + // 4. ВЫПОЛНЕНИЕ SQL-ЗАПРОСА + // ---------------------------------------------------- + $sql = $AVE_DB->Query(" + UPDATE + " . PREFIX . "_settings + SET + " . $set_string . " + WHERE + Id = 1 + "); + + if ($sql->_result === false) + { + $message = $AVE_Template->get_config_vars('SETTINGS_SAVED_ERR'); + $header = $AVE_Template->get_config_vars('SETTINGS_ERROR'); + $theme = 'error'; + } + else + { + $this->clearSettingsCache(); + + $message = $AVE_Template->get_config_vars('SETTINGS_SAVED'); + $header = $AVE_Template->get_config_vars('SETTINGS_SUCCESS'); + $theme = 'accept'; + reportLog($AVE_Template->get_config_vars('SETTINGS_SAVE_MAIN')); + } + + if (isset($_REQUEST['ajax']) && $_REQUEST['ajax'] == '1') + { + echo json_encode(array('message' => $message, 'header' => $header, 'theme' => $theme)); + } + else + { + $AVE_Template->assign('message', $message); + header('Location:index.php?do=settings&cp=' . SESSION); + } + + exit; +} /** * Метод отображения списка стран * diff --git a/functions/func.common.php b/functions/func.common.php index 4ae6216..2236d7f 100644 --- a/functions/func.common.php +++ b/functions/func.common.php @@ -265,6 +265,135 @@ function rrmdir($dir, &$result = 0) exit; } +/** + * Генерирует новый 32-байтовый ключ шифрования. + * @return string 64-символьная HEX-строка. + */ +function generate_encryption_key() { + // 32 байта для AES-256 + $bytes = openssl_random_pseudo_bytes(32, $cstrong); + + // Проверка на криптографическую надежность + if ($cstrong === false) { + return ''; + } + + return bin2hex($bytes); +} + +/** + * Создает файл ключа inc/smtp_key.php с уникальным ключом. + * @return string Сгенерированный ключ, или пустая строка в случае ошибки. + */ +function create_smtp_key_file() { + $key = generate_encryption_key(); + + if (empty($key)) { + return ''; + } + + $key_path = BASE_DIR . '/inc/smtp_key.php'; + $content = "Query(" - SELECT - # SETTINGS - * - FROM - " . PREFIX . "_settings - ", -1, 'settings', true, '.settings')->FetchAssocArray(); + if ($settings === null) + { + get_smtp_encryption_key(); - if ($field == '') - return $settings; + // Включаем параметры кэша (-1, 'settings', true, '.settings'). + $result = $AVE_DB->Query(" + SELECT * FROM " . PREFIX . "_settings + ", -1, 'settings', true, '.settings')->FetchAssocArray(); + + // 2. НОРМАЛИЗАЦИЯ: Преобразуем результат в одномерный массив $settings. + // Если кэш вернул двумерный массив (Array([0] => Array(...))), берем индекс [0]. + if (isset($result[0]) && is_array($result[0])) { + $settings = $result[0]; + } else { + // Если вернулся одномерный массив (например, при первом чтении из БД без кэша), используем его напрямую. + $settings = $result; + } + + // --- ЛОГИКА ДЕШИФРОВАНИЯ --- + // Теперь $settings гарантированно одномерный массив. + $row = $settings; + + if (isset($row['mail_type']) && $row['mail_type'] === 'smtp' && !empty($row['mail_smtp_pass'])) { + + $pass = trim($row['mail_smtp_pass']); - return isset($settings[$field]) - ? $settings[$field] - : null; - } + $decrypted_pass = decrypt_smtp_pass($pass); + + // Если дешифровка сработала, обновляем значение + if ($decrypted_pass !== $pass) { + $settings['mail_smtp_pass'] = $decrypted_pass; // Обновляем в статическом кэше $settings + } + } + } + + if ($field == '') + return $settings; + + return isset($settings[$field]) + ? $settings[$field] + : null; +} /** diff --git a/functions/func.mail.php b/functions/func.mail.php index 152087d..64d1ed7 100644 --- a/functions/func.mail.php +++ b/functions/func.mail.php @@ -141,14 +141,18 @@ use Symfony\Component\Mime\Address; break; case 'smtp': - $host = stripslashes(get_settings('mail_host')); - $port = (int)get_settings('mail_port'); - $user = stripslashes(get_settings('mail_smtp_login')); - $pass = stripslashes(get_settings('mail_smtp_pass')); + // --- ОПТИМИЗАЦИЯ: Получаем дешифрованные настройки одним вызовом --- + // Пароль уже ДЕШИФРОВАН функцией get_settings() + $settings = get_settings(); + + $host = stripslashes($settings['mail_host']); + $port = (int)$settings['mail_port']; + $user = stripslashes($settings['mail_smtp_login']); + $pass = stripslashes($settings['mail_smtp_pass']); + + // Получаем значение шифрования + $encrypt_setting = strtolower(stripslashes($settings['mail_smtp_encrypt'])); - // Получаем значение шифрования, которое теперь может быть 'tls_insecure' или 'ssl_insecure' - $encrypt_setting = strtolower(stripslashes(get_settings('mail_smtp_encrypt'))); - $scheme = 'smtp'; $encryption = ''; $extra_params = ''; @@ -170,7 +174,7 @@ use Symfony\Component\Mime\Address; $transport_dsn = sprintf('%s://%s:%s@%s:%d?encryption=%s%s', $scheme, urlencode($user), - urlencode($pass), + urlencode($pass), // Здесь используется ДЕШИФРОВАННЫЙ пароль $host, $port, $encryption, // 'tls' или 'ssl' @@ -180,7 +184,7 @@ use Symfony\Component\Mime\Address; // Без шифрования (когда выбрано "Нет") $transport_dsn = sprintf('smtp://%s:%s@%s:%d', urlencode($user), - urlencode($pass), + urlencode($pass), // Здесь используется ДЕШИФРОВАННЫЙ пароль $host, $port ?: 25 // Порт 25 по умолчанию ); diff --git a/inc/.htaccess b/inc/.htaccess new file mode 100644 index 0000000..224bfd2 --- /dev/null +++ b/inc/.htaccess @@ -0,0 +1,5 @@ +# Запретить прямой доступ к файлу, содержащему ключ шифрования + + Order Allow,Deny + Deny from all + \ No newline at end of file