dav 26.200.0 Безопасность

DAV 26.200.0: LOCK и UNLOCK на сетевом диске начали проверять права на запись

5 мин чтения

Обновление безопасности

Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.

В dav 26.200.0 заявлены два исправления сетевого диска: при подключении больше не спрашивают пароль повторно, а с файлами на диске снова можно работать (по диффу это правки в PUT, LOCK и UNLOCK). Попутно LOCK и UNLOCK начали проверять права на запись, которых раньше не проверяли вовсе, поэтому обновление нужно всем коробкам Битрикс24, где сотрудники подключают диск по WebDAV. Разработчикам стоит поискать у себя вызовы TokensTable::createToken() с явным токеном и CDav::GetWindowsVersion().

Заблокировать файл теперь можно только с правом на запись

CDavWebDavServer::LOCK() и UNLOCK() перед установкой и снятием блокировки вызывают новый private-метод checkWriteAccessByPath(). Для существующего объекта нужен canUpdate, для нового пути canAdd на родительской папке. Без прав клиент получит 403 Forbidden, а если объект или родительскую папку найти не удалось, 409 Conflict. Раньше проверки прав в этих методах не было, в LOCK она стояла закомментированной.

Токен блокировки getNewLockToken() теперь собирает UUID v4 из random_bytes(16). Раньше токен давал uuid_create() или md5(microtime().getmypid()), то есть хеш от времени и номера процесса. Про uuid_create() в комментарии к коду отдельно сказано, что OSSP-расширение может вернуть int.

Срок блокировки теперь задаёт клиент, но не больше часа. LOCK читает заголовок Timeout: Second-N и зажимает N между CDavWebDavServer::LOCK_TIMEOUT_MIN (60 секунд) и LOCK_TIMEOUT_MAX (3600). На Infinite и некорректное значение ставится LOCK_TIMEOUT_DEFAULT, те же 300 секунд, которые раньше получала любая блокировка.

CDavWebDav::CheckLockStatus() больше не останавливает пользователя его собственной блокировкой, даже если клиент не прислал токен. Владельца сверяют по новому числовому полю LOCK_USER_ID, чужие блокировки запрос по-прежнему не пропускают.

Сетевой диск перестаёт переспрашивать пароль

CDavWebDav для сетевого диска (CDavWebDavServer) отвечает на OPTIONS без авторизации на любом пути, раньше так было только для /. В комментарии к коду сказано, что ответ статичен, и дана ссылка на Mantis 133939 о том, что Office при discovery-запросе заново спрашивал пароль у уже подключённого диска. GroupDAV и CalDAV, как и раньше, пускают OPTIONS без авторизации только на /. Если мониторинг или сканер ждал 401 на OPTIONS к путям сетевого диска, больше он его не получит.

CDav::SetAuthHeader() больше не вызывает CHTTP::SetAuthHeader(). Заголовки собирает новый CDav::buildWwwAuthenticateHeaders(string $realm): array, он ничего не отправляет и только возвращает массив. В [0] всегда Basic, в [1] Digest, если в main включён use_digest_auth и CDav::isDigestEnabled() вернул true. Флаг сессии BX_HTTP_DIGEST_ABSENT намеренно больше не учитывается. По комментарию, из-за него схема авторизации «прыгала», и Windows снова просила пароль.

CDavResponse::Render() отправляет повторные WWW-Authenticate с replace=false. Раньше каждый header() шёл с заменой, и при паре Basic + Digest клиент получал только последний заголовок. Остальные заголовки ответа по-прежнему перезаписываются.

Не рассчитывайте на историю версий Windows по IP

CDav::GetWindowsVersion() получил тип возврата int и смотрит только на User-Agent текущего запроса. Регулярка стала Windows NT (\d+) вместо (\d{1}), так что Windows NT 10 больше не превращается в 1. Если в User-Agent нет Windows NT, метод вернёт 0. Раньше в этом случае версия бралась из истории по IP, которую модуль хранил сериализованной в опции dav/windows_version и перезаписывал при смене значения.

Если ваш код вызывает GetWindowsVersion(), готовьтесь к нулю на запросах без Windows NT. Опция больше не пишется, а её очистки в диффе нет, так что история по IP останется в базе. В ней лежат IP-адреса пользователей, поэтому опцию лучше удалить: \Bitrix\Main\Config\Option::delete('dav', ['name' => 'windows_version']).

Проверьте вызовы createToken() с явным токеном

Bitrix\Dav\TokensTable::createToken() при исключении на add() сначала ищет существующую строку по USER_ID и возвращает её. Новый TOKEN с повторной вставкой генерируется, только если строки нет. На MySQL USER_ID в b_dav_tokens уникален, поэтому если вы передаёте токен явно, а у пользователя уже есть запись, метод вернёт её как есть, со старыми TOKEN и EXPIRED_AT, даже истёкшими. Раньше такой вызов заканчивался исключением, потому что повторный add() падал на том же USER_ID. На PostgreSQL уникального ключа на USER_ID нет, там add() пройдёт, и у пользователя появится вторая строка. Берите токен из результата:

        <?php declare(strict_types=1);

namespace Vendor\Sync\Application\Service;

use Bitrix\Dav\TokensTable;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Main\Result;

final class DavTokenIssuer
{
    public function issue(int $userId, string $token): Result
    {
        $result = new Result();

        if (!Loader::includeModule('dav')) {
            return $result->addError(new Error('Модуль dav не установлен', 'DAV_MODULE_MISSING'));
        }

        $issued = (string)(TokensTable::createToken($userId, $token)['TOKEN'] ?? '');
        if ($issued === '') {
            return $result->addError(new Error('Токен не создан', 'DAV_TOKEN_EMPTY'));
        }

        // с 26.200.0 на MySQL при существующей записи вернётся её токен, переданный не сохранится
        return $result->setData([
            'token' => $issued,
            'reused' => $issued !== $token,
        ]);
    }
}

    

Параллельный PUT больше не падает на уникальном индексе

В PUT и PutCommit() три правки:

  • имя файла нормализуется через Bitrix\Disk\Ui\Text::correctFilename(), так же как это делает disk перед INSERT;
  • существующий файл ищет новый private loadLiveTwin() с фильтром DELETED_TYPE_NONE, и файл из корзины больше не считается существующим;
  • если параллельный PUT проиграл гонку на уникальном индексе (NAME, PARENT_ID), вместо ошибки в найденный файл загружается новая версия, при условии что у пользователя есть canUpdate.

generateUniqueName не включают специально, чтобы не плодить копии вида «file (1).docx».

Комментарий в PutCommit() утверждает, что MySQL сообщает о дубле обычной ошибкой (1062), а DuplicateEntryException бросает только PostgreSQL. В main той же сборки MysqlCommonConnection::createQueryException() уже превращает код 1062 в DuplicateEntryException, так что проверка подстроки (1062) работает как запасной вариант. В своём коде хватит ловить Bitrix\Main\DB\DuplicateEntryException и перечитывать строку победителя. Похожий приём есть в TokensTable::createToken() этого релиза (там ловится любое \Exception) и в private Driver::addStorageIfNotExistWithCreationStatus() из disk 26.1175.0. Приём подходит любому «создай, если нет»:

        <?php declare(strict_types=1);

namespace Vendor\Sync\Infrastructure\Repository;

use Bitrix\Main\DB\DuplicateEntryException;
use Vendor\Sync\Model\DeviceTable;

final class DeviceRepository
{
    // в b_vendor_sync_device уникальный ключ (USER_ID, CODE)
    public function getOrCreate(int $userId, string $code): array
    {
        $row = $this->find($userId, $code);
        if ($row !== null) {
            return $row;
        }

        try {
            DeviceTable::add(['USER_ID' => $userId, 'CODE' => $code]);
        } catch (DuplicateEntryException) {
            // параллельный запрос вставил строку раньше, берём его результат
        }

        return $this->find($userId, $code)
            ?? throw new \RuntimeException("Устройство {$code} не найдено после вставки");
    }

    private function find(int $userId, string $code): ?array
    {
        $row = DeviceTable::getList([
            'select' => ['ID', 'USER_ID', 'CODE'],
            'filter' => ['=USER_ID' => $userId, '=CODE' => $code],
            'limit' => 1,
        ])->fetch();

        return $row ?: null;
    }
}

    

Проверьте колонку LOCK_USER_ID и агент очистки

В b_dav_locks добавлено поле LOCK_USER_ID int NULL. Оно есть в install.sql для MySQL и PostgreSQL и в выборке по умолчанию CDavVirtualFileSystem::GetList(). Скрипта ALTER для существующих установок в файлах модуля нет. Updater-скрипты в файлы модуля не попадают, поэтому по диффу не проверить, как колонка появится на живой базе. UpdateLock() записывает LOCK_USER_ID при каждом продлении блокировки, если пользователь известен, поэтому старые строки без владельца дозаполнятся постепенно, без миграции. CAllDavVirtualFileSystem::Lock() и UpdateLock() получили необязательный последний параметр $userId = null, прежние вызовы работают.

Агент CDavVirtualFileSystem::collectExpiredLocks(); с интервалом 900 секунд регистрируется в DoInstall() и удаляется в DoUninstall(). Он вызывает protected purgeExpired(), тот одним DELETE FROM b_dav_locks WHERE EXPIRES < time() сносит все просроченные блокировки, а агент возвращает строку своего вызова. В комментарии агент назван страховкой к «ленивой» очистке. CheckLock() по-прежнему удаляет просроченную блокировку только для запрошенного пути. Других мест регистрации агента в диффе нет.

После обновления проверьте и колонку, и агент. Скрипт рассчитан на подключённое ядро, в командную PHP-строку админки его вставляйте без первой строки:

        <?php declare(strict_types=1);

use Bitrix\Main\Application;

$column = Application::getConnection()->getTableField('b_dav_locks', 'LOCK_USER_ID');
echo 'LOCK_USER_ID: ', $column !== null ? 'есть' : 'нет', PHP_EOL;

$agent = \CAgent::GetList([], ['=NAME' => 'CDavVirtualFileSystem::collectExpiredLocks();'])->Fetch();
echo 'агент: ', $agent ? 'следующий запуск ' . $agent['NEXT_EXEC'] : 'не зарегистрирован', PHP_EOL;

    

Без колонки GetList() падает на L.LOCK_USER_ID, а вместе с ним и CheckLock(), так что блокировки на сетевом диске работать не будут. Если колонки нет, добавьте её с тем же определением, что в install.sql:

        ALTER TABLE b_dav_locks ADD LOCK_USER_ID int NULL;

    

Если агента нет, его добавляет тот же вызов, что в DoInstall(): \CAgent::AddAgent('CDavVirtualFileSystem::collectExpiredLocks();', 'dav', 'N', 900).

Мелочи и находки

  • Токен из заголовка If: при продлении блокировки теперь разбирает private CDavWebDav::extractLockTokenFromIf(). Он понимает tagged- и untagged-формы, пропускает отрицания Not <token>, а если разобрать не вышло, режет строку по-старому.
  • В код ядра попали внутренние ссылки разработки: «See ADR Risk 1» в CDavResponse::Render() и уже упомянутый Mantis 133939 в webdav.php.

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

voximplant 26.800.0 Ломающее Свежее

Voximplant 26.800.0: attachRecord и finish отказывают локальным адресам, searchCrmEntities отдаёт один лид

В voximplant 26.800.0 у четырёх REST-методов внешней телефонии поменялось поведение, хотя сигнатуры остались прежними. `telephony.externalCall.attachRecord` и `telephony.externalCall.finish` с `RECORD_URL` больше не скачивают записи с приватных адресов и не ходят через прокси. `searchCrmEntities` от...

4 мин
timeman 26.100.0 Ломающее Свежее

Timeman 26.100.0: время считается по IANA-зоне сотрудника, у CTimeManEntry и REST другие значения

С timeman 26.100.0 учёт рабочего времени берёт зону сотрудника из IANA-идентификатора в `b_user.TIME_ZONE` и считает смещение UTC на момент каждого события, с переходами на летнее и зимнее время. Форматы полей и ответов остались прежними, поменялись значения: `TIME_START`, `TIME_FINISH` и `DURATION`...

3 мин
report 26.200.100 Безопасность Свежее

Report 26.200.100: закрыта SQL-инъекция через формат имени пользователя

Релиз «Конструктора отчётов» вендор описал фразой «Усилена безопасность модуля». На деле изменилась одна строка в `CReport::getFormattedNameExpr()`, теперь метод экранирует литералы из формата имени пользователя перед вставкой в SQL. Если модуль `report` установлен, обновите его до 26.200.100. Файл...

2 мин
note 26.400.0 Ломающее Свежее

note 26.400.0: updatedAt не отражает правки в редакторе, уведомления выключены по умолчанию

В [note 26.300.0](/bitrix-updates/note/note-26-300-0) модуль «Базы знаний 2.0» получил события, историю версий и подписки. Версия 26.400.0 добавляет избранное, обратные ссылки между документами, отдельную материализацию markdown-проекции Yjs-документа, асинхронный пересчёт поиска и боковой чат Bitri...

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

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

на связи

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

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

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

Войти