Socialnetwork 26.200.0: XSS в заявках и списке участников, копирование проекта по праву редактирования и курсор в log.blogpost.get
Обновление безопасности
Закрыта уязвимость или ослабленная проверка прав. Ставить в первую очередь.
Socialnetwork 26.200.0 закрывает XSS в списке участников проекта и в заявках в группы, подмену ссылок через название группы и утечку задач в контекст AI. Копировать проект теперь можно только с правом на редактирование. Администраторам, у которых стояли 26.100.0 или 26.150.0, стоит проверить права секретных групп. Если вы публикуете посты через REST без адресатов, храните длинные теги групп или скопировали шаблон socialnetwork.user_requests.ex в /local/, прочитайте разделы ниже до обновления.
Новое
Выгружайте ленту курсором
log.blogpost.get научился отдавать посты порциями по ID записи ленты. Передайте FIRST_ID, и выборка пойдёт по возрастанию ID, передайте LAST_ID, и она пойдёт от новых к старым. Ноль или пустое значение начинают обход с края, а если пришли оба параметра, побеждает FIRST_ID. LIMIT в этом режиме задаёт размер страницы, максимум 200. Дополнительно можно ограничить окно датами LOG_DATE_FROM и LOG_DATE_TO в ISO-8601 и пользовательском времени: FROM позже TO даст ошибку, верхнюю границу из будущего метод проигнорирует.
В курсорном режиме next в ответе содержит ID последней прочитанной записи ленты, а не поста. Конец выборки обозначает ответ без next. Страница может прийти неполной и даже пустой, но с next, если в ней попались неопубликованные посты. Тогда обход продолжается. Без курсора всё работает как раньше: сортировка по LOG_UPDATE и offset.
<?php declare(strict_types=1);
namespace Vendor\Feed\Sync;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
final class BlogPostPuller
{
// вебхук вида https://portal.example.ru/rest/1/<токен>/
public function __construct(private readonly string $webhook) {}
/** @return array{posts: array, cursor: int} */
public function pullNew(int $cursor): array
{
$body = (new HttpClient())->post($this->webhook . 'log.blogpost.get.json', [
'FIRST_ID' => $cursor, // 0 при первом запуске
'LIMIT' => 200,
]);
if ($body === false) {
return ['posts' => [], 'cursor' => $cursor];
}
$data = Json::decode($body);
// если в ответе нет next, лента дочитана; курсор сохраняйте между запусками
return [
'posts' => $data['result'] ?? [],
'cursor' => (int)($data['next'] ?? $cursor),
];
}
}
Откройте форму поста с выбранными адресатами
socialnetwork.api.livefeed.blogpost.getPostFormInitData принимает preselectedRecipients, JSON-массив кодов адресатов. В JS это одноимённая опция формы поста. Сервер пропускает только коды пользователей U<id>, список длиннее 1000 элементов отбрасывает целиком, а пустой массив открывает форму без адресатов, без отката к «Всем сотрудникам». Кто реально может получить пост, лента всё равно решает при сохранении.
Из своего модуля поддержку проверяйте через method_exists(ComponentHelper::class, 'isPostFormPreselectedRecipientsSupported'): модули выходят независимо, сигнатура action ничего не гарантирует.
Что закрыли
- Список участников проекта (
ProjectService::getMembers()) раскодировал имя пользователя черезhtmlspecialcharsbackвместо экранирования, теперь тамhtmlspecialcharsbx. - В заявках и приглашениях (
socialnetwork.user_requests.ex) имена экранируются один раз, ссылки больше не раскодируются, а путь к аватарке экранируется для CSS-стиля. - Название группы в тексте приглашения теперь экранируется и выводится как обычный текст, без разметки и ссылок.
Integration\AI\Controller::getTaskContext()перед тем, как отдать описание задачи и комментарии в контекст AI, проверяет право на чтение задачи. Для постов такая проверка уже была.- Агент
ReplaceAllUserAccessValueProjectAgentиз 26.100.0 переводил права «все пользователи» на задачах и ленте групп в «авторизованные», в том числе у секретных групп. Теперь секретным группам ставится «участники». Отката для групп, которые агент уже обработал, в релизе нет. Если у вас стояла 26.100.0 или 26.150.0, проверьте права на задачи и ленту у секретных групп руками.
Обновите копию шаблона заявок в /local/
Если шаблон socialnetwork.user_requests.ex скопирован к вам, правки до него не дойдут. Перенесите три вещи. Поля ~USER_NAME_FORMATTED и ~GROUP_NAME выводите как есть, компонент уже экранировал их ровно один раз. Уберите htmlspecialcharsback() вокруг USER_PROFILE_URL и GROUP_URL. Путь к аватарке перед вставкой в CSS сначала освободите от HTML-слоя, потом закодируйте всё, что закрывает строку, и только после этого экранируйте для атрибута. Наша адаптация нового шаблона: Замыкание вместо именованной функции нужно потому, что функция упадёт с «Cannot redeclare», если компонент на странице дважды. Тип параметра mixed: если файла нет, в поле картинки лежит false.
<?php
// в начале template.php
$cssBackground = static function (mixed $image): string {
$src = is_array($image) ? (string)($image['src'] ?? '') : '';
if ($src === '') {
return '';
}
// снимаем HTML-слой и кодируем всё, чем можно закрыть url('...')
$unsafe = [
"'" => '%27', '"' => '%22', '(' => '%28', ')' => '%29',
'\\' => '%5C', "\n" => '%0A', "\r" => '%0D', "\t" => '%09',
];
$src = strtr(htmlspecialcharsback($src), $unsafe);
return "background: url('" . htmlspecialcharsbx($src) . "') no-repeat 0 0;";
};
В разметке вызывайте его как style="<?=$cssBackground($arRequest["USER_PERSONAL_PHOTO_IMG"] ?? null)?>", и так же для GROUP_IMG.
Что сломается
Посты через REST без адресатов
Если в log.blogpost.add не передать ни DEST, ни SPERM, пост раньше уходил всем сотрудникам от любого интранет-пользователя. Теперь UA подставляется, только если у публикующего есть право писать всем, а это опции allow_livefeed_toall и livefeed_toall_rights. Иначе метод вернёт ошибку No destination specified. То же правило действует в AJAX-контроллере ленты. С настройками по умолчанию (Y и AU) ничего не меняется, сломаются интеграции на порталах, где «Всем сотрудникам» ограничили.
Передавайте адресатов явно.
Копирование проекта
copyAction() контроллера проектов V2 проверял у исходного проекта только чтение, теперь требует право на редактирование. Раньше пользователь, который проект только видит, мог его скопировать, теперь получит отказ.
Длинные ключевые слова и теги групп
CSocNetGroup::CheckFields() кодирует эмодзи в KEYWORDS и проверяет длину уже закодированной строки, лимит 255 символов. При превышении Add и Update вернут false с исключением в $APPLICATION. Такой же валидатор получило ORM-поле WorkgroupTagTable.NAME, а WorkgroupTagTable::set() вернёт false, если хоть один тег длиннее 255 символов. Писать через WorkgroupTable по-прежнему нельзя, зато KEYWORDS из его getList() теперь приходят с раскодированными эмодзи.
Эмодзи в базе хранится как :f09f9880:, это 10 символов вместо одного. Импорт групп с длинными тегами раньше проходил, теперь упадёт. Прямые SQL-запросы к b_sonet_group.KEYWORDS и b_sonet_group_tag.NAME увидят эмодзи в закодированном виде.
Мелочи и находки
- Группы без типа (
TYPE IS NULL) теперь везде определяются поSCRUM_MASTER_ID > 0.CSocNetGroup::Update()больше не перезаписываетTYPEпри каждом вызове и сам дописывает тип в строки, где его нет. ОбработчикиOnSocNetGroupUpdateполучают тип из базы. - 16 вызовов устаревших
CHTTP::urlAddParams()иurlDeleteParams()в компонентах и контроллере ленты заменили наBitrix\Main\Web\Uri:(string)(new Uri($url))->deleteParams(['WFILES'])->addParams(['IFRAME' => 'Y']). В своём коде можно сделать то же. - Фича Note в проектах закрыта для коллаберов, а при копировании проекта задачи теперь всегда переносятся с чек-листами.