vote · Опросы, голосования 26.300.0 Безопасность

Vote 26.300.0: список проголосовавших отдаётся только по подписи

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

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

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

В Vote 26.300.0 закрыли утечку списка проголосовавших через попап «Кто проголосовал», в том числе в анонимных опросах, и ужесточили права на удаление и сброс опросов в админском списке. Обновиться стоит всем, у кого на портале есть опросы. Если вы копировали к себе шаблон .userfield компонента voting.current, после обновления попап в копии опустеет, и копию придётся обновить.

Новое

Подписывайте контекст списка

Bitrix\Vote\Service\VotedContextSigner упаковывает ID опроса, ID варианта ответа, шаблон имени и шаблон ссылки на профиль в JSON и подписывает через Main\Security\Sign\Signer:

  • sign(int $voteId, int $answerId, string $nameTemplate = '', string $urlTemplate = ''): string;
  • unsign(string $signedContext): array возвращает voteId, answerId, nameTemplate и urlTemplate, а на битой подписи или битом содержимом бросает BadSignatureException.

Шаблон результатов подписывает контекст при рендере и кладёт его в data-sign счётчика ответа. Обработчик берёт вариант ответа и шаблоны только из подписи.

Получайте проголосовавших через сервис

Bitrix\Vote\Service\VotedListService отдаёт список по подписи:

  • getListBySign(?string $sign, array $params): array возвращает страницу проголосовавших за вариант ответа: карточки с именем, фото 21×21, ссылкой на профиль и типом (mail или extranet). Параметры iNumPage, nPageSize (по умолчанию DEFAULT_PAGE_SIZE = 10) и requestId. Результат кешируется на 600 секунд. Пустая или битая подпись даёт пустой список без исключения.
  • getVoterIdsBySign(?string $sign, array $params): array возвращает только ID, без профилей и кеша.
  • Статические buildCacheId(int $answerId, int $nPageSize, int $iNumPage) и buildCacheDir(int $voteId) строят ID и папку кеша (/vote/<voteId>/voted/).

Скрытые голоса сервис не отдаёт. Анонимность он проверяет у самого опроса, поэтому опрос, который сделали анонимным уже после голосования, тоже не покажет, кто за что голосовал.

Если вам нужен свой список проголосовавших, подпишите контекст в шаблоне через (new VotedContextSigner())->sign($voteId, $answerId, '#NAME# #LAST_NAME#', '/company/personal/user/#USER_ID#/') и отдавайте страницы своим контроллером:

        <?php declare(strict_types=1);

namespace Vendor\Polls\Infrastructure\Controller;

use Bitrix\Main\Engine\ActionFilter;
use Bitrix\Main\Engine\Controller;
use Bitrix\Main\Error;
use Bitrix\Main\Loader;
use Bitrix\Vote\Service\VotedListService;

final class Voters extends Controller
{
    public function configureActions(): array
    {
        return [
            'list' => [
                'prefilters' => [
                    new ActionFilter\Authentication(),
                    new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
                    new ActionFilter\Csrf(),
                ],
            ],
        ];
    }

    public function listAction(string $sign, int $page = 1): ?array
    {
        if (!Loader::includeModule('vote')) {
            $this->addError(new Error('Модуль vote не установлен', 'VOTE_MODULE_MISSING'));

            return null;
        }

        $list = (new VotedListService())->getListBySign($sign, ['iNumPage' => $page, 'nPageSize' => 20]);

        return [
            'users' => array_map(
                static fn(array $item): array => [
                    'id' => $item['ID'],
                    'name' => $item['FULL_NAME'],
                    'url' => $item['URL'],
                    'photo' => $item['PHOTO_SRC'],
                ],
                $list['items'],
            ),
            'hasMore' => $list['StatusPage'] === 'continue',
        ];
    }
}

    

В PHOTO сервис кладёт готовый тег <img>, а адрес картинки лежит в PHOTO_SRC, его контроллер и отдаёт.

Что сломается

Обновите свою копию шаблона .userfield

Проверьте копии шаблона .userfield и вложенных в него voting.result и voting.form в шаблоне сайта или в /local, а также свой JS, который ходил в users.php этого шаблона.

users.php теперь принимает подпись в POST-поле sign вместо прежних параметров. Без валидной подписи он возвращает пустой список, на запрос в старом формате тоже. Ответ стал JSON-ом через Json::encode с Content-Type: application/json, раньше был CUtil::PhpToJsObject с application/x-javascript.

В шаблоне voting.result у ссылки-счётчика вместо атрибута rel теперь data-sign. В анонимном опросе оба шаблона, voting.result и voting.form, выводят вместо ссылки <span>, и список проголосовавших там не открывается.

В JS из BX.Vote.init и конструктора BVotedUser убраны urlTemplate и nameTemplate, шаблоны теперь едут внутри подписи. Клик по счётчику обрабатывается, только если у него есть rev. changeData находит счётчик с любым тегом, а атрибуты ссылки выставляет только у <a>.

Старая копия шлёт запрос без подписи, и попап «Кто проголосовал» открывается пустым. Проще всего заново скопировать шаблон из bitrix/components/bitrix/voting.current/templates/.userfield/ и перенести в него свои правки. Если правок в копии много, перенесите из ядра script.js вместе с script.min.js и component_epilog.php, а счётчик в своём voting.result выводите так:

        <?php
use Bitrix\Vote\Service\VotedContextSigner;
use Bitrix\Vote\Vote\Anonymity;

$signer = new VotedContextSigner();
$isAnonymous = (int)($arResult['VOTE']['ANONYMITY'] ?? 0) === Anonymity::ANONYMOUSLY;
?>
<?php foreach ($arQuestion['ANSWERS'] as $answer): ?>
    <?php if ($isAnonymous): ?>
        <span class="bx-vote-voted-users"><?= (int)$answer['COUNTER'] ?></span>
    <?php else: ?>
        <a href="javascript:void(0);" class="bx-vote-voted-users" rev="<?= (int)$answer['ID'] ?>"
           data-sign="<?= htmlspecialcharsbx($signer->sign(
               (int)$arParams['VOTE_ID'],
               (int)$answer['ID'],
               (string)($arParams['~NAME_TEMPLATE'] ?? ''),
               (string)($arParams['~PATH_TO_USER'] ?? ''),
           )) ?>"><?= (int)$answer['COUNTER'] ?></a>
    <?php endif ?>
<?php endforeach ?>

    

В voting.form повторите штатное ветвление: для анонимного опроса <span class="bx-vote-voted-users"> вместо ссылки.

users.php из своей копии удалите или замените ядровым. Штатный JS обращается к ядровому файлу, а старая копия так и остаётся со старым кодом.

Свой JS переведите на отправку sign из data-sign и разбирайте ответ как обычный JSON.

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

  • В админском списке опросов (voting.admin.votes) удаление и сброс опроса возвращают false, если у пользователя закрыт доступ к модулю или если у него нет права W на модуль и нет ни одного канала с правом 4 и выше. Список опросов не запрашивается, если у пользователя без W нет каналов на чтение.
  • Шаблон ссылки на профиль из подписи принимается, только если начинается с / или http(s)://. Иначе подставляется /company/personal/user/#USER_ID#/.
  • Кеш списка переехал в /vote/<voteId>/voted/, тег прежний, vote_form_vote_<voteId>. В ID кеша зашит VISIBILITY_RULE_VERSION = 2, поэтому списки, закешированные по старым правилам видимости, не переиспользуются.
  • Для автотестов появились data-testid: vote-current-answer-counter-<ID> у счётчиков и vote-current-voted-users-popup у попапа.
  • Deprecated в релизе нет, схема БД не менялась. Версия выросла с 26.200.0, VERSION_DATE у новой 2026-09-16.

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

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

Подписаться

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

translate 26.100.0 Безопасность Свежее

Translate 26.100.0: импорт CSV и редактор переводов больше не пишут за пределы разрешённых папок

В Translate 26.100.0 закрыли выход за пределы разрешённых папок при записи языковых файлов через редактор и CSV-импорт модуля «Перевод». Обновиться стоит всем, у кого модуль установлен. Публичный API не ломается, но CSV-импорт теперь пропускает файлы с путями вне разрешённых папок.

2 мин
timeman 26.300.0 Ломающее Свежее

Timeman 26.200.0–26.300.0: выключенный «Учёт рабочего времени» отключает REST timeman.*, перерывы за период отдаёт RecordProvider

Timeman 26.200.0 и 26.300.0 пришли в коробочный Битрикс24 в один прогон. В 26.200.0 модуль начал слушаться настройки портала: если инструмент «Учёт рабочего времени» выключен, перестают работать REST `timeman.*`, контроллеры и компоненты модуля, а уведомления не уходят. В 26.300.0 появился публичный…

2 мин
tasks 26.350.0 Рутинное Свежее

Tasks 26.350.0: NEW_COMMENTS_COUNT читается точечно

В tasks 26.350.0 модуль иначе читает `NEW_COMMENTS_COUNT`, значения при этом прежние. Это касается `CTaskItem::fetchList()` и `fetchListArray()` с этим полем в select, а значит и `Manager\Task::getList()` с REST-методом `tasks.task.list`, и REST-метода `tasks.task.get`. Раньше ради этого поля `Count…

1 мин
superset 26.500.0 Заметное Свежее

Superset 26.500.0: проверка готовности сервера и повтор GET после быстрого 5xx

Superset 26.500.0 из коробочного Битрикс24 нужен тем, кто обращается из своего кода к собственному (self-hosted) серверу BI Конструктора. У публичных провайдеров два новых метода: `ServerProvider::checkReadiness()` проверяет, что Superset видит свою базу метаданных, а `DatasetProvider::listPhysical(…

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

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

на связи

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

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

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

Войти