Disk 26.1100.0: закрыты дыры в правах на публичные ссылки и версии, единые ссылки больше не выключаются
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
Disk 26.1100.0 вышел в коробку 2 сентября 2026 года (по официальному каналу «Битрикс24 changelog») и закрывает три дыры в правах. Любой, кто видел файл, мог поставить или снять пароль и срок действия его публичной ссылки, подпись файла _uls открывала всю историю его версий, а вложения к комментариям встреч читал кто угодно. Ещё модуль включил единые ссылки безусловно. Опции unified_link.enabled и unified_link.allow_type_* больше не читаются, так что портал, где они не стояли в Y, после обновления единые ссылки получит.
Разработчикам придётся проверить наследников Folder, DownloadController и CheckReadPermission, позиционные вызовы в Internal\Service\UnifiedLink и скрипты, которые удаляют папки. Сборка датирована 4 августа 2026 года, дифф от 26.1000.0 затрагивает 496 файлов, +29297/−10271 строк, причём JS и компоненты посчитаны дважды: в bitrix/js и bitrix/components и ещё раз в install/.
Какие дыры закрыты
Настройки публичной ссылки больше не меняются с одним правом чтения
Контроллер Controller\ExternalLink проверял права префильтром-замыканием, которое требовало на объекте только canRead. Поэтому setPasswordAction, setDeathTimeAction, revokePasswordAction, revokeDeathTimeAction и disallowEditDocumentAction мог вызвать любой, кто видит файл, а canUpdate проверялся только внутри allowEditDocumentAction.
Замыкание заменили фильтром Internals\Engine\ActionFilter\CheckExternalLinkSettingsPermission. Он требует BaseObject::canManageExternalLink(), то есть чтение плюс canUpdate или включённую у объекта опцию «управлять публичным доступом при чтении». Для allowEditDocument и disallowEditDocument нужен именно canUpdate. Права проверяются на реальном объекте (getRealObject()).
Все экшены контроллера заодно получили типы возврата. Класс объявлен final, так что наследников у него быть не может.
Подпись файла больше не открывает его версии
DownloadController::checkPermissions() принимал подпись _uls, выданную на файл, и по versionId отдавал любую версию этого файла. По комментарию в коде, подпись файла отдаётся в JSON каждого файла, так что вместе с документом открывалась вся его история. Теперь для доступа к версии по _uls нужна подпись пары «файл + версия» (hasUnifiedLinkAccess()). Такие подписи выдаёт и проверяет новый Internal\Service\UnifiedLink\UnifiedLinkSignature.
Фильтр Internals\Engine\ActionFilter\CheckReadPermission пропускал версию, у которой нет файла-владельца (continue). В 26.1100.0 checkVersion() считает это ошибкой чтения, а для версии фильтр принимает _uls пары. Битая подпись вместо BadSignatureException даёт обычный отказ.
Если ваш код строил ссылки на версии с подписью самого файла, к версиям эта подпись больше не подходит.
Вложения встреч требуют роли во встрече
Uf\ForumMessageConnector::canRead() кешировал результат одним значением на объект коннектора, и этот ответ получал любой следующий $userId. Теперь кеш хранится отдельно для каждого пользователя.
Вложения к комментариям встреч (MEETING, MEETING_ITEM) раньше читал кто угодно, если сообщение лежало в форуме комментариев встреч (meeting:comments_forum_id). Теперь для чтения нужна роль во встрече, её определяет CMeeting::getUserRole. canUpdate() этого коннектора вызывает canRead(), поэтому и правка таких вложений требует той же роли.
Единые ссылки больше не выключаются
В Internal\Service\UnifiedLink\Configuration методы isEnabled() и isFileTypeAllowed() теперь всегда возвращают true, а supportsUnifiedLink() проверяет только наличие UNIQUE_CODE. Опции unified_link.enabled и unified_link.allow_type_2…unified_link.allow_type_12 модуль больше не читает, install/index.php их тоже не выставляет. Раньше модуль считал выключенной любую из этих опций, кроме Y, в том числе незаданную. Если на портале они не равны Y, после обновления единые ссылки включатся для всех типов файлов, и этими опциями их уже не выключить.
Посмотреть, какие из этих опций выключены у вас, можно до обновления:
<?php declare(strict_types=1);
use Bitrix\Main\Config\Option;
$names = ['unified_link.enabled'];
foreach (range(2, 12) as $type) {
$names[] = 'unified_link.allow_type_' . $type;
}
// До 26.1100.0 выключенной считалась любая опция, кроме 'Y', в том числе незаданная
$switchedOff = array_values(array_filter(
$names,
static fn (string $name): bool => Option::get('disk', $name, 'N') !== 'Y',
));
print_r($switchedOff);
Непустой список значит, что после обновления эти настройки перестанут действовать.
Проверьте вызовы Internal\Service\UnifiedLink
У FileHandler\HtmlRenderableFileHandlerFactory::createHandler() удалили шестой параметр bool $forceExternal. На его место встал bool $deferred, а в конец добавился ?ExternalLink $externalLink. Позиционный вызов, который передавал forceExternal, теперь включает отложенную загрузку. Анонимный посетитель, для которого внешнюю ссылку не передали, получает DefaultHtmlRenderableFileHandler, а не ExternalLinkHandler. Передавайте аргументы по имени и отдавайте фабрике саму публичную ссылку:
<?php declare(strict_types=1);
// $factory: HtmlRenderableFileHandlerFactory, $link: Bitrix\Disk\ExternalLink
$handler = $factory->createHandler(
file: $file,
deferred: false,
externalLink: $link,
);
Конструктор FileHandler\ExternalLinkHandler теперь требует ссылку вторым параметром:
__construct(File $file, ExternalLink $externalLink, ?AttachedObject $attachedObject = null, ?Version $version = null)
Старый вызов new ExternalLinkHandler($file) упадёт с ArgumentCountError.
Папку почтовых вложений удалит только система
Папку с кодом FOR_MAIL_ATTACHMENTS и любое дерево, где она лежит, удаляет или отправляет в корзину только системный пользователь. Для остальных Folder::canDelete() и canMarkDeleted() возвращают false, а удаление и пометка удаления заканчиваются ошибкой DISK_FOLDER_22001. У методов удаления появился параметр $bypassDeletionRestriction:
Folder::deleteTree($deletedBy, bool $bypassDeletionRestriction = false)
Folder::deleteNonRecursive($deletedBy, bool $bypassDeletionRestriction = false)
FolderLink::deleteTree($deletedBy, bool $bypassDeletionRestriction = false)
Storage::delete($deletedBy, bool $bypassDeletionRestriction = false)
Наследник Folder со старой сигнатурой deleteTree() или deleteNonRecursive() с новым родителем несовместим. SocialnetworkHandlers::onUserDelete() удаляет хранилище пользователя с bypassDeletionRestriction: true. Если ваш код удаляет хранилища не от имени системного пользователя, передавайте тот же флаг.
Ещё deleteTree() и markDeleted() останавливаются на первом потомке, которого не удалось удалить, и поднимают его ошибки наверх. Раньше цикл шёл дальше. Скрипт очистки больше не удалит «всё, что получится», поэтому проверяйте результат:
<?php declare(strict_types=1);
use Bitrix\Disk\Folder;
use Bitrix\Main\Loader;
Loader::requireModule('disk');
$folder = Folder::loadById($folderId);
if ($folder !== null && !$folder->deleteTree($userId)) {
$logger->warning('Папка удалена не полностью', [
'folderId' => $folderId,
'mailAttachmentsInside' => $folder->getErrorByCode('DISK_FOLDER_22001') !== null,
'errors' => array_map(static fn ($error) => $error->getMessage(), $folder->getErrors()),
]);
}
Найдите в своём коде $this->signer и ERROR_NO_AI_MODULE
Из DownloadController и Internals\Engine\ActionFilter\CheckReadPermission удалили свойство protected Signer $signer, вместо него появилось protected UnifiedLinkSignature $unifiedLinkSignature. Наследник, который обращался к $this->signer, сломается.
В Internal\Service\MarkdownRenderService удалена константа ERROR_NO_AI_MODULE, вместо неё ERROR_RENDERER_UNAVAILABLE с кодом DISK_MARKDOWN_RENDER_UNAVAILABLE. Parsedown теперь берётся из модуля ui (ui/vendor/erusev/parsedown), а не из ai, и Configuration::isEnabledMarkdownViewer() проверяет MarkdownRenderService::isAvailable() вместо Loader::includeModule('ai'). RENDER_FORMAT_VERSION поднят с 2 до 3, так что старый кеш рендера сбросится. Санитайзер Markdown теперь пропускает <em>.
Эти обращения и вызовы обработчиков единых ссылок ищутся одной командой из корня сайта:
grep -rnE '\$this->signer|ERROR_NO_AI_MODULE|new ExternalLinkHandler\(|->createHandler\(|function (deleteTree|deleteNonRecursive)\(' local/
Новое
HTML-просмотрщик приехал выключенным
В модуле появился просмотрщик HTML-файлов, по умолчанию выключенный. Он состоит из нескольких частей:
- экшены
showHtmlAction()вController\File,Controller\VersionиController\AttachedObjectс теми же префильтрами, что уdownload: без CSRF, сAuthentication(true)иCloseSession; - сервисы в
lib/Internal/Service/:HtmlViewerServiceотдаёт файл изолированным документом,HtmlViewerPolicyрешает, что открывать (htmlиhtmсTypeFile::KNOWN), рядомHtmlViewerPageServiceиHtmlViewerSharingPolicy; - адреса строят
UrlManager::getUrlForShowHtml(),getUrlForShowHtmlAttached()иgetUrlForShowHtmlVersion(); - компонент
disk.file.viewer-html, рендерерUI\Viewer\Renderer\Html, обработчикFileHandler\HtmlFileHandler,FileExtensionMap::Htmlи префикс единой ссылкиUnifiedLinkPrefix::Html(/html/); - трейт
Infrastructure\Controller\HtmlViewerRefusalResponse, с ним отказ фильтра приходит HTML-заглушкой, а не JSON.
Настройки лежат в lib/configuration.php: isEnabledHtmlViewer() (опция disk_enable_html_viewer, по умолчанию N), getMaxSizeForHtmlViewer() (disk_html_viewer_max_size, 10 МБ по умолчанию) и getHtmlViewerAllowedHosts() (disk_html_viewer_csp_hosts).
Санитайзера у просмотрщика нет, HtmlViewerService отдаёт HTML байт в байт. Изолируют документ заголовки. Content-Security-Policy начинается с sandbox allow-scripts, за ним идут default-src 'none', connect-src 'none', form-action 'none', frame-ancestors 'self' и webrtc 'block'. По умолчанию из внешних источников разрешены только шрифты и стили Google Fonts, хосты из disk_html_viewer_csp_hosts добавляются к style-src, font-src и img-src. Iframe в шаблоне disk.file.viewer-html тоже получил sandbox="allow-scripts". Хосты из disk_html_viewer_csp_hosts проходят проверку #^https://[a-z0-9.-]+$#, поэтому дописать через опцию свою директиву в заголовок не получится.
Документ, переименованный в .html, как text/html не отдаётся. Тип файла вычисляется при загрузке и при переименовании не пересчитывается, так что у такого файла остаётся TypeFile::DOCUMENT, и HtmlViewerPolicy его не пропустит.
Пока просмотрщик выключен, UnifiedLinkPrefix::forHtml() оставляет HTML-файлы на префиксе /file/. Маршрут /html/ живёт в модуле intranet, и портал, где обновили только disk, отвечал бы на такие ссылки 404. В эталоне маршрут уже есть в intranet 26.1350.0 (html в шаблоне fileType в intranet/install/routes/intranet.php). Прежде чем включать опцию, убедитесь, что intranet на портале знает маршрут /html/. Исключение /html/ из редиректа экстранет-пользователей разобрано отдельно, в extranet 26.300.0.
По публичной ссылке HTML-файл открывается в том же просмотрщике, только без окна настройки доступа.
Настройки публичной ссылки можно заблокировать
У публичной ссылки появился флаг CAN_EDIT_SETTINGS (колонка в b_disk_external_link, по умолчанию 1) и методы ExternalLink::canEditSettings() и getCanEditSettings(). Если флаг снят, changePassword(), changeDeathTime(), revokePassword(), revokeDeathTime(), changeAccessRight() и changeCanDownloadWithReadAccess() возвращают false с ошибкой DISK_EL_22001 (ExternalLink::ERROR_SETTINGS_EDIT_DENIED).
Для объектов из папки почтовых вложений BaseObject::addExternalLink() всегда пишет CAN_EDIT_SETTINGS = false, что бы ни прислал клиент. Controller\AccessRights не даёт поменять canEditSettings у существующей ссылки и отклоняет правку настроек заблокированной. При создании ссылки права, пароль, TTL и canDownloadWithReadAccess теперь применяются в том же запросе.
Код, который меняет настройки ссылок, может отличить блокировку от прочих сбоев по коду ошибки:
<?php declare(strict_types=1);
use Bitrix\Disk\ExternalLink;
use Psr\Log\LoggerInterface;
function setLinkPassword(ExternalLink $link, string $password, LoggerInterface $logger): bool
{
if ($link->changePassword($password)) {
return true;
}
$logger->warning('Пароль публичной ссылки не изменён', [
'linkId' => $link->getId(),
'settingsLocked' => $link->getErrorByCode(ExternalLink::ERROR_SETTINGS_EDIT_DENIED) !== null,
]);
return false;
}
В своём интерфейсе canEditSettings() поможет заранее спрятать форму пароля и срока.
Пароль публичной ссылки теперь проверяется без перезагрузки страницы. В install/components/bitrix/disk.external.link/ajax.php появился DiskExternalLinkAjaxController::checkPasswordAction($hash, $password) с фильтрами POST и CSRF, авторизация ему не нужна. Подтверждение пароля хранится 24 часа в ExternalLinkPasswordService.
disk.file.search научился фильтровать по типу файла
REST-метод disk.file.search принимает фильтр FILTER[FILE_TYPE], но только при TYPE=file. Значение передаётся строкой или списком из document, image, video, audio, archive, pdf, vector_image, board, known и unknown. Добавлены ошибки SearchError::invalidFileType() и incompatibleFileType(). На стороне PHP у Search\StorageFileFinderOptions появился параметр ?array $typeFileValues.
У почтовых вложений своя папка
В SpecificFolder появились код CODE_FOR_MAIL_ATTACHMENTS = 'FOR_MAIL_ATTACHMENTS' и проверки isSpecificFolder() и isSpecificFolderCode(), в Storage метод getFolderForMailAttachments(). Запрет удаления собран из правил: сервис Internal\Service\Folder\DeletionRestriction, интерфейс DeletionRestrictionRule и правило MailAttachmentsFolderDeletionRestrictionRule.
Модуль mail в эталоне уже пользуется этой папкой. mail/lib/integration/disk/reallargeattachmentstorage.php вызывает getFolderForMailAttachments(), а largeattachmentstoragefactory.php страхуется method_exists() на случай старого disk.
Что ещё добавили
- Vibeoffice ведёт список тех, кто сейчас в документе.
Document\Vibeoffice\Service\PresenceServiceиPresenceAccessServiceобрабатываютenter,heartbeatиleave, регистрация живёт 90 секунд, лимиты 8 на сессию, 16 на пользователя и 100 на область.SavedContentSynchronizerдожидается сохранённой версии перед открытием просмотра,Controller\Vibeoffice::waitForSavedAction()принимает POST с JSON. ВDocumentSessionManagerдобавленcloneSessionWithCurrentContentVersion(),persistContentVersionSnapshot()получил параметр?File $file = null, аfindOrCreateSession()перечитывает файл перед поиском сессии. УDocumentSessionпоявилисьgetExternalLinkId()иmatchesExternalContext(). - Для бизнес-процессов появился триггер
DiskFileCreateTrigger(install/activities/bitrix/diskfilecreatetrigger/), ему нуженCBPRuntime::ACTIVITY_API_VERSION >= 4. РядомIntegration\Bizproc\Starter\DiskModuleSettingsиBizProcDocument::getStarterModuleSettings(). Bitrix24Disk\TreePathResolver(@internal) строит пути для снапшотов десктопного клиента поb_disk_object_pathи не собирает дерево в памяти. По докблоку, класс на 827 строк заменяет тяжёлый JOIN трёх таблиц с EXISTS-проверкой прав, который выполнялся при каждом снапшоте под нагрузкой. Включается опциейsnapshot_path_resolver_enabled, по умолчаниюN.Internal\Service\UnifiedLink\ExternalLinkContext::create()иresolve()работают с подписанным маркеромexternalLinkContext, по нему единая ссылка открывается в контексте публичной.UnifiedLinkAccessService::check()иAccessCheckHandlerFactory::create()получили необязательные?ExternalLinkи?Version.- По мелочи:
User::getExternalAuthId(),Ui\Avatar::preload(),Ui\Icon::getIconSetNameByFile()иgetIconSetNameByObject(),FileAttributes::buildByFileId(..., ?Version $version = null),FileResolver::resolveVersion(),getHeaders()уRenderResultиFileHandlerOperationResult, JS-расширениеdisk.board-create, шаблонstandaloneуdisk.error.pageи шаблонairуdisk.breadcrumbs.
БД
Схема описана в install/migrations/tables.php:
b_disk_external_link: новая колонкаCAN_EDIT_SETTINGS tinyint NOT NULL DEFAULT 1, в ORMInternals\ExternalLinkTableэто boolean со значением по умолчаниюtrue;b_disk_document_session: колонкаEXTERNAL_LINK_ID intи индексIX_DISK_DS_EXTERNAL_LINK_ID (EXTERNAL_LINK_ID, ID), вDocumentSessionTableдобавленоIntegerField('EXTERNAL_LINK_ID'). Если поле не передали,DocumentSession::add()берёт его изCONTEXT.
install/db/mysql/install.sql и install/db/pgsql/install.sql не менялись, модуль ставится через installMigrations() в install/index.php. Как колонки появятся на уже работающих порталах, по диффу не видно, апдейтер в снапшот не попадает.
Новые опции: disk_enable_html_viewer, disk_html_viewer_max_size, disk_html_viewer_csp_hosts, snapshot_path_resolver_enabled. Из install/index.php убраны Option::set для unified_link.enabled и unified_link.allow_type_*.
Мелочи и находки
SocialnetworkHandlers::onAfterUserUpdate()больше не переименовывает хранилища внешних пользователей (EXTERNAL_AUTH_IDизUserTable::getExternalUserTypes()). По комментарию в коде, открытые линии переписывают имя контакта при каждом входящем сообщении.Controller\AttachedObject::configureActions()теперь начинается сparent::configureActions(). Раньше метод возвращал толькоdownloadиshowMarkdown.Folder::processAdd()ловитDuplicateEntryExceptionвместо поиска(1062)в тексте ошибки. На PostgreSQL подбор уникального имени обёрнут в транзакцию.- Публичную страницу ссылки (
install/components/bitrix/disk.external.link/) переписали, появились шаблоныaccess-card,folder-listиfile-viewers/html. Номер страницы в списке папки теперь ограничен сверху. Раньше большое значение переполняло int и давало отрицательный LIMIT. - В комментариях к HTML-просмотрщику есть ссылки на внутренние документы «ADR § 6» и «API-01».
- В пересобранном
disk.uploader.uf-file.bundle.jsпоявился TODO загружать расширения disk и disk.document по требованию. - Новых
@deprecatedнет. Единственная пометка в диффе пришла из пересобранногоpromo-boost.bundle.js, а в исходнике методChecker.shouldShowPromoSessionBoost()был помечен@deprecatedи до релиза.
Что делать
- Обновите disk. На старой версии читатель файла может менять пароль и срок его публичной ссылки, а подпись файла открывает все его версии.
- До обновления проверьте опции
unified_link.enabledиunified_link.allow_type_*. Если какая-то из них не равнаY, предупредите пользователей, что единые ссылки включатся. - Найдите и поправьте в
local/обращения к$this->signerиERROR_NO_AI_MODULE, переопределенияdeleteTree()иdeleteNonRecursive(), позиционные вызовыcreateHandler()иnew ExternalLinkHandler($file). - В скриптах очистки Диска проверяйте результат
deleteTree()и ошибкуDISK_FOLDER_22001. - Ссылки на версии файлов подписывайте парой «файл + версия» через
UnifiedLinkSignature. - HTML-просмотрщик включайте опцией
disk_enable_html_viewer, только когда intranet на портале знает маршрут/html/. - Если ставите сразу и следующую версию модуля, прочитайте разбор disk 26.1150.0.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
Bitrix\Disk\DownloadController::$signer
|
Удалено |
Bitrix\Disk\DownloadController::$unifiedLinkSignature
|
Bitrix\Disk\Internals\Engine\ActionFilter\CheckReadPermission::$signer
|
Удалено |
Bitrix\Disk\Internals\Engine\ActionFilter\CheckReadPermission::$unifiedLinkSignature
|
Bitrix\Disk\Internal\Service\MarkdownRenderService::ERROR_NO_AI_MODULE
|
Удалено |
Bitrix\Disk\Internal\Service\MarkdownRenderService::ERROR_RENDERER_UNAVAILABLE
|
Bitrix\Disk\Internal\Service\UnifiedLink\FileHandler\HtmlRenderableFileHandlerFactory::createHandler($forceExternal)
|
Удалено |
Bitrix\Disk\Internal\Service\UnifiedLink\FileHandler\HtmlRenderableFileHandlerFactory::createHandler($externalLink)
|