Security 26.200.0: одноразовый пароль можно получить на резервный email
Ломающее обновление
Удалены или изменены публичные API — прикладной код может перестать работать.
В security 26.200.0 у push-OTP появился третий канал доставки кода. Раньше код приходил пушем в мобильное приложение или по SMS, теперь его можно получить письмом на резервный адрес, который пользователь подтверждает кодом. Канал выключен по умолчанию, так что сам релиз ничего не включит. Внимательно обновляться стоит тем, кто наследует Otp или PushOtp, и тем, кто подключает файлы модуля по путям, потому что файлы в lib/ переименованы в PascalCase.
До 25 сентября на этой странице была заметка о том, что security 26.200.0 не ставится на маке и Windows. 25 сентября обновление у нас встало, но ошибка в ядре, из-за которой падал апдейтер, никуда не делась. Подробности в разделе «Если ставите обновления на маке или Windows».
Проверьте пути к файлам модуля
Релиз переименовал 25 файлов в lib/ и два lang-файла, поменяв только регистр. lib/mfa/otp.php стал lib/Mfa/Otp.php, lib/filter/auditor/xss.php стал lib/Filter/Auditor/Xss.php, lib/xscanresulttable.php стал lib/XScanResultTable.php, и так по всем таблицам, фильтрам и классам MFA. В include.php карту автозагрузки для Bitrix\Security\XScanResults и XScanResult поправили под новое имя.
Наш эталон стоит на регистронезависимой ФС, поэтому проверить поведение на Linux по нему нельзя. Если установщик на Linux удалил старые пути, require_once по пути вида /bitrix/modules/security/lib/mfa/otp.php упадёт, так что проверьте свой код на прямые подключения файлов из security/lib. Контроллеры lib/controller/pushotp.php и lib/controller/xscan.php в нашем эталоне остались в нижнем регистре.
Если держите ядро в git на маке, проверьте индекс после обновления. Git на регистронезависимой ФС не замечает переименований, где меняется только регистр. У нас в коммит снапшота они не попали, и 27 путей security пришлось фиксировать отдельным коммитом.
Если ставите обновления на маке или Windows
24 августа апдейтер security 26.200.0 упал у нас с фаталом:
PHP Fatal error: Cannot redeclare class Bitrix\Main\Mail\Internal\EventMessageTable
(previously declared in .../main/lib/Mail/Internal/EventMessageTable.php:33)
in .../main/lib/Mail/Internal/EventMessage.php on line 33
Тогда эталон работал в Docker на macOS, папка сайта была подключена через bind-mount. 25 сентября обновление встало на том же маке в нативном окружении, без Docker, на PHP 8.4. Ошибка в ядре при этом осталась, и нативный PHP на macOS падает на ней точно так же.
Падает CEventType::GetListEx() из main. Он добавляет к запросу связь с сущностью Bitrix\Main\Mail\Internal\EventMessage, записанной без суффикса Table. ORM проверяет это имя через class_exists(), и автозагрузчик ищет файл lib/Mail/Internal/EventMessage.php. Следом он пробует путь целиком в нижнем регистре, lib/mail/internal/eventmessage.php. На Linux с одним каталогом lib/Mail/Internal/ файла нет ни по одному из путей, class_exists() возвращает false, и Entity::getInstance() дописывает к имени Table. На регистронезависимой ФС (APFS на macOS по умолчанию, NTFS на Windows) уже первый путь, lib/Mail/Internal/EventMessage.php, открывает старый файл eventmessage.php. В нём объявлен тот же класс EventMessageTable, который к этому моменту уже загружен из EventMessageTable.php. 25 сентября, уже после обновления, мы получили тот же фатал одним вызовом GetListEx() с фильтром по EVENT_NAME.
CEventMessage::Add() и Update() через CheckFields() вызывают GetListEx(), когда в полях есть EVENT_NAME. В августе апдейтер security упал на первом же Add(), когда создавал шаблон USER_OTP_AUTH_CODE. В сентябре он дошёл до конца, скорее всего, потому, что шаблоны USER_OTP_AUTH_CODE и USER_OTP_EMAIL_CONFIRM уже лежали в нашей базе с 19 сентября и создавать их не пришлось. Откуда они там взялись до установки, мы не восстановили. Ваш код на такой ФС упадёт так же на любом Add() и на Update() с EVENT_NAME в полях.
Такая же ссылка без Table, на Bitrix\Main\Mail\Internal\EventType, есть в CEventMessage::GetList(). Если в процессе до этого уже загружен EventTypeTable, чтение шаблонов падает с Cannot redeclare class ...EventTypeTable, мы это воспроизвели. По коду на такой ФС упадут и сохранение почтового шаблона в админке (message_edit.php), и открытие типа почтового события (type_edit.php); в браузере мы это не проверяли.
После августовского падения в b_event_type у нас остались типы USER_OTP_AUTH_CODE и USER_OTP_EMAIL_CONFIRM, а в b_event_message шаблонов не было. Если апдейтер падал и у вас, проверьте обе таблицы.
На Linux и Windows мы это не воспроизводили. По коду прод на Linux падать не должен, а на Windows с NTFS фатал должен повториться, как на маке. Для локальной разработки держите сайт на регистрозависимом томе: на маке это отдельный том или образ в формате APFS (Case-sensitive), в Docker это named volume вместо bind-mount папки проекта. Удалять старые файлы ядра в нижнем регистре руками не советуем, это правка ядра.
Обновите наследников Otp и PushOtp
Класс Bitrix\Security\Mfa\Otp не final, и сигнатуры у него поменялись:
isOtpEnabled(),isRecoveryCodesEnabled(),isPushPossible()получили возвращаемый тип: bool;- protected
setDeactivateUntil($date)теперьsetDeactivateUntil(?Type\DateTime $date); - protected
setInitialDate(Type\DateTime $date)теперьsetInitialDate(?Type\DateTime $date); setUserInfo(array $userInfo)дополнительно читает ключиEMAILиDATE_SENT_EMAILи выставляет даты инициализации и деактивации без проверки на пустоту.
Два вида переопределений стали несовместимы с родителем, и PHP упадёт на них с фаталом прямо при объявлении класса. Во-первых, это isOtpEnabled(), isRecoveryCodesEnabled() и isPushPossible() без : bool. Во-вторых, setInitialDate() с ненуллабельным параметром Type\DateTime $date: родитель теперь принимает null, и параметр наследника оказывается строже родительского. Исправленное переопределение для первого случая:
<?php declare(strict_types=1);
namespace Vendor\Security;
use Bitrix\Main\Config\Option;
use Bitrix\Security\Mfa\Otp;
final class CorporateOtp extends Otp
{
public static function isOtpEnabled(): bool // до 26.200.0 работало и без ": bool"
{
return parent::isOtpEnabled()
&& Option::get('vendor.security', 'otp_enabled', 'Y') === 'Y';
}
}
У контроллера Bitrix\Security\Controller\PushOtp protected-метод getOtpCode(int $userId) стал getOtpCode(Otp $otp). Объект OTP теперь достаёт новый getOtp(), а ошибку OTP_CODE при пустом результате добавляет сам getOtpCode(). Экшен sendSms раньше добавлял её сам и теперь этого не делает, новый sendEmail тоже рассчитывает на getOtpCode(). Если вы переопределяли метод, поправьте сигнатуру и проверьте, что ошибка не потерялась и не задвоилась.
Разберитесь, как работает резервный email
Цепочка состоит из трёх экшенов PushOtp.
sendConfirmationEmailAction(string $email)вызывает авторизованный пользователь. Ядро проверяет адрес черезcheck_email(), генерирует короткий код черезBitrix\Main\Authentication\ShortCodeи отправляет письмоUSER_OTP_EMAIL_CONFIRM. В ответ приходитsignedData, то есть адрес, подписанныйParameterSignerс сольюpush_otp_email.confirmEmailAction(string $code, string $signedData)снимает подпись, принимает только шестизначный код, проверяет егоShortCode::verify()и сохраняет адрес вb_sec_user.EMAIL.sendEmailAction()работает на шаге входа без префильтра авторизации, как иsendSms. Он отправляет текущий код OTP письмомUSER_OTP_AUTH_CODEне чаще раза в 60 секунд. Время последней отправки лежит вb_sec_user.DATE_SENT_EMAIL, в ответе приходят замаскированный адрес видаa***b@***.ruиtimeLeft.
В письмо с кодом входа попадают дата, логин, устройство, браузер, платформа, User-Agent, IP, страна, регион, город и ссылка на справку.
Задать резервный адрес можно и в карточке пользователя, в поле «Резервный email для одноразовых паролей». Поле показывается, только когда Otp::isPushPossible() возвращает true, а сохранить его может пользователь с операцией security_edit_user_otp. Этот путь проверяет только формат адреса через check_email(), кода подтверждения он не требует.
Каналы и журнал настраиваются на странице security_otp.php:
otp_allow_emailвключает отправку по email, по умолчаниюN;otp_allow_smsотвечает за SMS, по умолчаниюY; если выключить,sendSmsAction()вернёт ошибкуSMS_DISABLED;otp_log_sendingпишет каждую отправку в журнал событий с типомSECURITY_OTP_SENDING. Вdefault_option.phpэтой опции нет, но код считает её включённой, пока значение неN. В описание события уходит номер телефона или адрес получателя.
Шаблоны USER_OTP_AUTH_CODE и USER_OTP_EMAIL_CONFIRM установщик создаёт неактивными ('ACTIVE' => 'N'), так что вместе с опцией загляните и в почтовые шаблоны. Заодно перечитайте письмо подтверждения. Оно просит вставить код «в Битрикс24» и подписано «Ваш Битрикс24». Если ваш продукт называется иначе, пользователь получит письмо про чужой продукт, и текст придётся поправить.
Найдите пользователей без резервного адреса
В b_sec_user две новые колонки, EMAIL varchar(255) и DATE_SENT_EMAIL datetime, и обе есть в ORM Bitrix\Security\Mfa\UserTable. Список тех, у кого включён push-OTP, а резервного адреса пока нет:
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\Security\Mfa\OtpType;
use Bitrix\Security\Mfa\UserTable;
Loader::requireModule('security');
$withoutBackupEmail = UserTable::query()
->setSelect(['USER_ID', 'LOGIN' => 'USER.LOGIN'])
->where('ACTIVE', true)
->where('TYPE', OtpType::Push->value)
->whereNull('EMAIL')
->fetchAll();
Колонки описаны в install.sql для новой установки. Как их добавляет обновление на живом портале, из диффа не видно, скрипт обновления в артефакты не попадает. На нашем эталоне после обновления обе колонки на месте, как и обработчик Otp::getAuditTypes() на OnEventLogGetAuditTypes.
Мелочи и находки
UserTable::TYPEтеперь знает проpush: в список допустимых значений добавилиOtpType::Push->valueрядом сtotpиhotp.- Ключ кеша OTP сменился с
otp_users_{id}наotp_users_{id}_v1, а выборка пошла по*, поэтому старый кеш без новых полей не подхватится. - Подписи типов журнала
SECURITY_OTPиSECURITY_OTP_SENDINGтеперь отдаётOtp::getAuditTypes(), зарегистрированный наmain:OnEventLogGetAuditTypesвDoInstall(). СтарыйCSecurityFilter::GetAuditTypes()всё ещё возвращаетSECURITY_OTP, хотя фразу для него изlang/ruиlang/en(classes/general/filter.php) удалили. XScanResultTable::trace, белый список стека вызовов для сканера xscan, переписан под PascalCase-пути main:Engine/AutoWire/Binder.php,Engine/Controller.php,HttpApplication.php.meta/orm.phpперегенерирован: классыEO_*явно наследуютEntityObjectиCollection, у коллекций появились аннотацииfind()иfilter(). Пути в маркерахORMENTITYANNOTATIONостались в нижнем регистре.- Страница
security_otp.phpсохраняет настройки только по POST-полямsave,applyиotp_siteb, а не по$_REQUEST. - Повторная отправка письма раньше срока отдаёт текст SMS-таймаута («Время ожидания еще не истекло.»), а в
sendEmailAction()осталось$geoData ['cityName']с пробелом перед скобкой.
Что делать
- Поищите
requireиincludeфайлов изbitrix/modules/security/lib/по путям в нижнем регистре. - Если наследуете
Otp, добавьте: boolк переопределённымisOtpEnabled(),isRecoveryCodesEnabled()иisPushPossible(), а параметр переопределённогоsetInitialDate()сделайте?Type\DateTime. Если наследуетеPushOtp, поправьтеgetOtpCode(). - Для email-канала включите
otp_allow_email, проверьте активность и текст шаблоновUSER_OTP_AUTH_CODEиUSER_OTP_EMAIL_CONFIRM. - Если обновляете ядро на маке или Windows, переносите сайт на регистрозависимый том. Иначе фатал
Cannot redeclare classнаEventMessageTableилиEventTypeTableможет прилететь из апдейтера или из вашего кода, который читает, создаёт или правит почтовые шаблоны черезCEventMessageили вызываетCEventType::GetListEx().