SMTP password security fix

This commit is contained in:
2025-10-11 17:17:12 +05:00
parent e622f7e6fa
commit 9d427f4870
4 changed files with 378 additions and 133 deletions

View File

@@ -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;
}
/**
* Метод отображения списка стран
*

View File

@@ -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 = "<?php\n// Ключ шифрования. НЕ МЕНЯТЬ после первого использования!\n// Сгенерирован: " . date('Y-m-d H:i:s') . "\ndefine('SMTP_ENCRYPTION_KEY', '{$key}');";
// Пытаемся записать файл
if (file_put_contents($key_path, $content) === false) {
return '';
}
// @chmod($key_path, 0400); // Была причиной ошибок, оставляем закомментированной
// Определяем константу, чтобы не читать файл снова
if (!defined('SMTP_ENCRYPTION_KEY')) {
define('SMTP_ENCRYPTION_KEY', $key);
}
return $key;
}
/**
* Получает ключ шифрования SMTP, читая файл inc/smtp_key.php.
* @return string Ключ шифрования (64 символа), или пустая строка, если ключ отсутствует.
*/
function get_smtp_encryption_key() {
if (defined('SMTP_ENCRYPTION_KEY')) {
return SMTP_ENCRYPTION_KEY;
}
$key_path = BASE_DIR . '/inc/smtp_key.php';
if (!file_exists($key_path)) {
return '';
}
$content = @file_get_contents($key_path);
if (preg_match('/define\(\s*\'SMTP_ENCRYPTION_KEY\'\s*,\s*\'([a-fA-F0-9]{64})\'\s*\)/', $content, $matches)) {
$key = $matches[1];
define('SMTP_ENCRYPTION_KEY', $key);
return $key;
}
return '';
}
/**
* Шифрует данные (SMTP-пароль).
* @param string $data Нешифрованный пароль.
* @return string Зашифрованная строка в Base64 (IV + Ciphertext), или исходные данные в случае ошибки.
*/
function encrypt_smtp_pass($data)
{
$key = get_smtp_encryption_key();
if (empty($data) || empty($key)) {
return $data;
}
$cipher = 'aes-256-cbc';
$ivlen = openssl_cipher_iv_length($cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext = openssl_encrypt($data, $cipher, $key, OPENSSL_RAW_DATA, $iv);
if ($ciphertext === false) {
return $data;
}
return base64_encode($iv . $ciphertext);
}
/**
* Дешифрует данные (SMTP-пароль).
* @param string $data Зашифрованная строка в Base64.
* @return string Дешифрованный пароль, или исходная строка в случае ошибки/отсутствия ключа.
*/
function decrypt_smtp_pass($data)
{
$key = get_smtp_encryption_key();
if (empty($data) || empty($key)) {
return $data;
}
$decoded = base64_decode($data, true);
if ($decoded === false) {
return $data;
}
$cipher = 'aes-256-cbc';
$ivlen = openssl_cipher_iv_length($cipher);
if (strlen($decoded) < $ivlen) {
return $data;
}
$iv = substr($decoded, 0, $ivlen);
$ciphertext_raw = substr($decoded, $ivlen);
$decrypted = openssl_decrypt($ciphertext_raw, $cipher, $key, OPENSSL_RAW_DATA, $iv);
return $decrypted !== false ? $decrypted : $data;
}
/**
* Получение основных настроек
@@ -272,28 +401,54 @@ function rrmdir($dir, &$result = 0)
* @param string $field параметр настройки, если не указан - все параметры
* @return mixed
*/
function get_settings($field = '')
{
global $AVE_DB;
function get_settings($field = '')
{
global $AVE_DB;
static $settings = null;
static $settings = null;
if ($settings === null)
$settings = $AVE_DB->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;
}
/**

View File

@@ -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 по умолчанию
);

5
inc/.htaccess Normal file
View File

@@ -0,0 +1,5 @@
# Запретить прямой доступ к файлу, содержащему ключ шифрования
<Files "smtp_key.php">
Order Allow,Deny
Deny from all
</Files>