Landing 26.1200.0: подписанный предпросмотр ушёл в CSP-песочницу, а импорт с заменой сайта помечает старые страницы удалёнными
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
Landing 26.1200.0 трогает 690 файлов, но основная масса там картинки, переводы и пересобранные бандлы. Релиз закрывает дыры в подписанной ссылке предпросмотра, чинит импорт с заменой сайта и размечает для скринридеров интерфейсы прав, настроек и редактора. Если вы вызываете AiSiteChatAvailabilityService или Controller\Copilot::executeGenerationAction() из своего кода, начните с первых двух разделов.
Уберите вызовы старых методов AiSiteChatAvailabilityService
Из Bitrix\Landing\Integration\AiAssistant\Service\AiSiteChatAvailabilityService удалены три публичных метода: checkSitesAiChatAvailability(int $bindingId), isSitesAiChatAvailable(int $bindingId) и getSitesAiChatUnavailableReason(int $bindingId). Deprecated-стадии не было, вызов упадёт сразу. Сам класс теперь помечен @internal, так что опираться на него в своём коде стоит с оглядкой.
Появился isSitesAiChatPotentiallyAvailable(): bool, но заменяет он старые методы только в интерфейсе. По комментарию к методу, это проба для отрисовки, не привязанная к сущности и не учитывающая права, а действия над конкретным сайтом надо проверять методами, которые знают про привязку.
Старый isSitesAiChatAvailable($bindingId) шёл через checkSitesAiChatAvailability(), а тот проверял две вещи подряд: checkSitesAiProductAvailability() и checkTriggerAvailability($bindingId). Оба метода остались, и эту связку можно собрать самим:
<?php declare(strict_types=1);
use Bitrix\Landing\Integration\AiAssistant\Service\AiSiteChatAvailabilityService;
use Bitrix\Landing\Rights;
$service = new AiSiteChatAvailabilityService();
// показать ли точку входа в AI-чат: только для отрисовки, без прав и без привязки
$showEntryPoint = $service->isSitesAiChatPotentiallyAvailable();
// замена isSitesAiChatAvailable($bindingId): продукт плюс триггер для привязки
$canStart = $service->checkSitesAiProductAvailability()->isAvailable()
&& $service->isTriggerAvailable($bindingId)
// права на сайт проверяем отдельно
&& Rights::hasAccessForSite($siteId, Rights::ACCESS_TYPES['edit']);
Раньше checkTriggerAvailability() ловил любое исключение из проверки включённости AI-сайтов и возвращал ERROR_TRIGGER_UNAVAILABLE, а теперь try/catch там нет, и исключение уходит к вам.
В диффе старый метод вызывался в одном месте, в result_modifier.php стартовой страницы, причём как isSitesAiChatAvailable(1), с захардкоженным bindingId = 1.
Вызывайте executeGeneration только POST-запросом с CSRF
Bitrix\Landing\Controller\Copilot::executeGenerationAction(int $generationId) был статическим, теперь это обычный метод экземпляра. Для действия executeGeneration в configureActions() добавлены фильтры Csrf и HttpMethod([POST]). Появилась и проверка владельца. Если автор генерации не текущий пользователь, действие вернёт false с ошибкой ACCESS_DENIED («Insufficient permissions to execute generation.»).
Статический вызов Copilot::executeGenerationAction($id) из PHP после обновления упадёт, GET-запрос к действию отсечёт фильтр, а чужую генерацию запустить больше не выйдет.
Не рассчитывайте на свой код в подписанном предпросмотре
Компонент landing.pub различает теперь предпросмотр по валидной подписанной ссылке (Site::isPublicHashValid) и DRAFT_MODE баз знаний и групп. Для подписанной ссылки в Битрикс24 он отдаёт заголовок:
Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals allow-popups-to-escape-sandbox allow-downloads
Без allow-same-origin у страницы непрозрачный origin, и до cookie, хранилища и API портала она не дотянется. По docblock sandboxSignedPreview(), под origin портала скрипт страницы выполнялся с сессией портала того, кто открыл ссылку. Своё превью устройств редактор запирает в sandbox-фрейм ровно поэтому, а по прямой ссылке эта страница открывалась вообще без песочницы.
От фишинга песочница не спасает. Форме входа, которую автор нарисует на адресе портала, посетитель поверит, поэтому в режиме песочницы (Landing::getDevicePreviewMode(), он теперь включается и для подписанной ссылки, и для sandbox-фрейма превью в редакторе) ядро выключает код автора:
Hook\Page\HeadBlock::enabled()иHook\Page\GTM::enabled()возвращаютfalse;Hook\Page\B24button::enabled()тоже возвращаетfalse, но новая проверка стоит ниже ветки с раннимreturn true, и в этой ветке виджет остаётся включённым (условие ветки в хунк не попало). Про виджет в комментарии сказано, что егоCODE«is stored as sent (Hook::saveData() checks no option list)»;- HTML-блок санируется через новый
LandingBlocksHtmlComponent::mustSanitize(), даже если у сайта свой домен. Раньше сайт со своим доменом получал сырой HTML и в публикации, и в предпросмотре.
Если ваши блоки или компоненты выводят произвольный код автора, стоит вести себя так же:
<?php declare(strict_types=1);
namespace Vendor\Landing\Render;
use Bitrix\Landing\Landing;
final class PartnerWidgetRenderer
{
public function render(string $authorHtml): string
{
// в песочнице предпросмотра ядро глушит код автора, не выбиваемся из общего поведения
if (Landing::getDevicePreviewMode()) {
return '';
}
return $authorHtml;
}
}
Ещё три правки вокруг предпросмотра:
- Подписанный «хвост»
preview/<hash>/теперь ставится только на ссылки своего сайта.Landing::getPublicUrl()проверяет это новым protectedisPreviewSite(), и маркеры#landing<id>/#block<id>, ведущие на страницы другого сайта, превращаются в обычный публичный URL. Комментарий объясняет, что подписанный хвост открывает весь черновик сайта. - Редирект по
forceLandingIdна ветке «страница не найдена» раньше делалLanding::createInstance()по id из запроса и уводил туда без проверок. Теперь новыйgetForceReloadUrl()требует, чтобы страница существовала, принадлежала этому же сайту и прошлаcheck_permissions, если он включён у компонента. Иначе, как сказано в комментарии, подписанный предпросмотр одного сайта превращался в подписанную ссылку на черновик другого. - У облачного сайта со своим доменом ссылка предпросмотра строится на хосте портала, в виде
https://<portal>/pub/site/<id>/preview/<hash>/. Хеш подписан парой «хост портала + путь публикации», и новый классSite\PreviewUrlследит, чтобы хост ссылки и хост подписи не разъезжались. Раньше в такие ссылки подставлялся домен сайта.
Проверьте импорт с заменой сайта
Шаг Transfer\Script\Action\UpdateReplacedSitePages должен при замене сайта убирать его старые страницы. Условие в нём было перевёрнуто, if ($siteId > 0) return;, так что при замене сайта шаг не делал ничего. Теперь стоит $siteId <= 0, и при замене шаг выполняется:
- снимает области страниц через
TemplateRef::deleteArea()и помечает страницы удалёнными черезLanding::markDelete(); - если
markDelete()вернул ошибку, возвращает области через новыйTemplateRef::restoreArea()и бросаетTransferException; - выполняется в финальном блоке сценария
ReplaceSite, послеUpdateSpecialPagesи передActivateRights.
Соседний шаг CheckReplacedSite раньше проверял только, что id заменяемого сайта передан. Теперь он требует siteId > 0 и право edit на этот сайт (Rights::hasAccessForSite()) и выполняется после SetContextUser. Текст ошибки остался прежним, «Replaced site ID is required», так что при отказе в правах сообщение будет сбивать с толку. Vibe\Installer теперь передаёт в импорт ключ replaceSiteId вместо siteId.
Новое
TemplateRef::deleteArea($lid) теперь возвращает массив снятых записей (без ID), а TemplateRef::restoreArea(array $areas) умеет вернуть их обратно. Пара пригодится, если вы сами удаляете страницы и хотите откатиться при ошибке:
<?php declare(strict_types=1);
namespace Vendor\Landing\Service;
use Bitrix\Landing\Landing;
use Bitrix\Landing\TemplateRef;
use Bitrix\Main\Result;
final class PageRemover
{
public function remove(int $landingId): Result
{
$areas = TemplateRef::deleteArea($landingId);
$deleted = Landing::markDelete($landingId);
if (!$deleted->isSuccess()) {
TemplateRef::restoreArea($areas);
}
return $deleted;
}
}
Bitrix\Landing\Site\PreviewUrl — новый final класс без состояния, все методы статические: isCloudTarget(), isCloudPreview(), getPortalOrigin(), withRequestPort(), getSitePath(), buildSiteBase(), buildPreviewBase(). Например, withRequestPort() возвращает к «голому» хосту порт из запроса, но только если порт числовой и относится к тому же хосту:
<?php declare(strict_types=1);
use Bitrix\Landing\Site\PreviewUrl;
PreviewUrl::withRequestPort('portal.example.ru', 'portal.example.ru:8080'); // 'portal.example.ru:8080'
PreviewUrl::withRequestPort('portal.example.ru', 'evil.example.ru:8080'); // 'portal.example.ru'
getPortalOrigin() вне веб-запроса возвращает пустую строку, запасной хост ядро не подставляет. По комментарию, ссылка на чужом хосте выглядела бы целой, но проверку подписи не прошла бы.
Компонент landing.blocks.message принимает новый параметр MESSAGE_TYPE (status или alert, по умолчанию status), который выводится как role сообщения. MESSAGE при этом по-прежнему выводится сырым, так что текст экранируйте сами:
$APPLICATION->IncludeComponent('bitrix:landing.blocks.message', '', [
'MESSAGE' => htmlspecialcharsbx($errorText),
'MESSAGE_TYPE' => 'alert',
]);
Мелочи и находки
- Большой проход по доступности. В
landing.settingsпоявились live-регион для ошибок,aria-busyи фокус на блоке фатальной ошибки. Вlanding.rolesиlanding.role_editдобавилисьaria-label, заголовки таблиц сscopeиaria-required. Вoptions.phpполя получили<label for>, у верхней панели редактора кнопки устройств сообщаютaria-pressed, а вlanding.landing_designblockкнопки из<span>стали<button>. Комментарии к этим правкам местами длиннее самих правок. - Шаблоны обросли атрибутами
data-testid. Похоже на подготовку к UI-автотестам, но в диффе это нигде не сказано. - Экранирование по дороге:
$helpUrlиsrciframe вlanding.landing_designblockиsrciframe вlanding.landing_viewтеперь проходятhtmlspecialcharsbx. Вlanding.role_editназвание роли берётся из~CURRENT, чтобы не кодировать его дважды. - В
options.phpподпись поля получает префикс раздела, только если фраза уже переведена. В комментарии сказано, что фраза «is newer than the locales of the product». - Favicon публичной страницы собирается как
rtrim(SITE_RELATIVE_URL, '/') . '/favicon.ico'. Раньше при адресе без завершающего слеша имя файла приклеивалось к последнему сегменту пути. - У корзины сайтов появилось пустое состояние (
landing.sites,landing.site_tile, новый параметрTOTAL_COUNT). - Из
install/images/landing/удалены 212 картинок: 199 фотографийbusiness/*, 10 логотипов и 3 паттерна. По stat.txt столько же ушло из/bitrix/images/landing/, а вmigration_config.jsonпоявилось сопоставлениеinstall/images→images. Ссылаются ли на эти файлы блоки, по диффу не видно. - Инлайн-шрифт иконок для песочницы предпросмотра теперь собирается один раз на вендора (
Icon::addInlineVendorAssets()), раньше его перечитывали для каждого блока. - Дата сборки 26.1200.0 — 17 августа 2026, раньше обоих патчей 26.1100.100 (20 августа) и 26.1100.200 (4 сентября). Правки контроллера файлов Диска из 26.1100.200 этот дифф не трогает, они остаются на месте.
- Deprecated нет, схема БД не менялась.
Что делать
- Поищите в коде
isSitesAiChatAvailable,checkSitesAiChatAvailability,getSitesAiChatUnavailableReasonиexecuteGenerationAction. - Прогоните импорт с заменой сайта на тестовом портале.
- Откройте подписанный предпросмотр своего сайта в Битрикс24 и посмотрите, как он выглядит без кода автора.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
Bitrix\Landing\Integration\AiAssistant\Service\AiSiteChatAvailabilityService::isSitesAiChatAvailable()
|
Удалено |
Bitrix\Landing\Integration\AiAssistant\Service\AiSiteChatAvailabilityService::isSitesAiChatPotentiallyAvailable() (только для UI; для действий checkSitesAiProductAvailability() + isTriggerAvailable())
|
Bitrix\Landing\Integration\AiAssistant\Service\AiSiteChatAvailabilityService::checkSitesAiChatAvailability()
|
Удалено | — |
Bitrix\Landing\Integration\AiAssistant\Service\AiSiteChatAvailabilityService::getSitesAiChatUnavailableReason()
|
Удалено | — |