Security 26.200.0: одноразовый пароль можно получить на резервный email

6 мин чтения
Узнавайте о ломающих изменениях Битрикса раньше, чем они доедут до прода: свежие разборы — в канале @bxmax_updates

Ломающее обновление

Удалены или изменены публичные 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.

  1. sendConfirmationEmailAction(string $email) вызывает авторизованный пользователь. Ядро проверяет адрес через check_email(), генерирует короткий код через Bitrix\Main\Authentication\ShortCode и отправляет письмо USER_OTP_EMAIL_CONFIRM. В ответ приходит signedData, то есть адрес, подписанный ParameterSigner с солью push_otp_email.
  2. confirmEmailAction(string $code, string $signedData) снимает подпись, принимает только шестизначный код, проверяет его ShortCode::verify() и сохраняет адрес в b_sec_user.EMAIL.
  3. 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'] с пробелом перед скобкой.

Что делать

  1. Поищите require и include файлов из bitrix/modules/security/lib/ по путям в нижнем регистре.
  2. Если наследуете Otp, добавьте : bool к переопределённым isOtpEnabled(), isRecoveryCodesEnabled() и isPushPossible(), а параметр переопределённого setInitialDate() сделайте ?Type\DateTime. Если наследуете PushOtp, поправьте getOtpCode().
  3. Для email-канала включите otp_allow_email, проверьте активность и текст шаблонов USER_OTP_AUTH_CODE и USER_OTP_EMAIL_CONFIRM.
  4. Если обновляете ядро на маке или Windows, переносите сайт на регистрозависимый том. Иначе фатал Cannot redeclare class на EventMessageTable или EventTypeTable может прилететь из апдейтера или из вашего кода, который читает, создаёт или правит почтовые шаблоны через CEventMessage или вызывает CEventType::GetListEx().

Новая версия вышла — разбор уже в Telegram

В канале @bxmax_updates публикуем каждый свежий разбор: что сломается, что устарело и что появилось в API. Узнаете до того, как обновите прод.

Подписаться

Читайте дальше

security 26.300.0 Безопасность Свежее

Security 26.300.0: xscan ищет файлы-маркеры взлома и снова работает на новом main

Security 26.300.0 чинит и усиливает сканер «Поиск троянов» (xscan). Он ищет семь файлов, которые остаются на сайте после взлома, проверяет файлы, чьё имя отличается от системного только регистром, и снова запускается на main 26.900.0 и новее. А на порталах с push-OTP и обязательной 2FA резервный ema…

2 мин
ui 26.700.0 Безопасность Свежее

UI 26.696.0–26.700.0: закрыты дыры в правах на карточки, Lexical 0.50 и подсказки ui.system

Вышли две версии ui: 26.696.0 научила холст `ui.block-diagram` копировать блоки, а 26.700.0 на 5290 файлов принесла расширения `ui.system`, Lexical 0.50 и права доступа V2 со значениями из entity-selector. Severity security из-за 26.700.0, где закрыты копирование чужого представления карточки, выдач…

2 мин
translate 26.100.100 Рутинное Свежее

Translate 26.100.100: страница «Перевод языковых файлов» не падает на FTS-таблице удалённого языка, skipCache в getLanguages() работает в каждом вызове

Translate 26.100.100 вышел патчем в две строки после [26.100.0](/bitrix-updates/translate/translate-26-100-0), публичный API и схема БД не менялись. У внутреннего (`@internal`) класса `Bitrix\Translate\Config` метод `getLanguages(skipCache: true)` раньше обходил кэш, только если вызов был первым за…

1 мин
sale 26.600.0 Безопасность Свежее

Sale 26.600.0: закрыты дыры в правах контроллеров и REST, появился провайдер покупателей

Как и [26.500.0](/bitrix-updates/sale/sale-26-500-0), Sale 26.600.0 закрывает пачку дыр в безопасности, на этот раз в правах контроллеров и REST. Контроллеры статистики интеграции с Б24 и синхронизатора не проверяли права на модуль sale, а `sale.order.payment.change` открывал чужую оплату; REST заяв…

2 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…

Можно закрыть чат и перейти на другую страницу. Когда ответ будет готов, на кнопке Домового появится отметка.

Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти