corsik corsik

Безопасность: устранение уязвимостей в контроллерах

В модулях «Расчёт стоимости доставки» (corsik.yadelivery) и «Подсказки DaData» (corsik.suggestions) обнаружена и закрыта группа уязвимостей: AJAX-контроллеры администрирования были доступны без проверки прав, а также присутствовали path traversal, неконтролируемая запись в файл, запись произвольных опций и XSS на страницах админки.

Важно

Эта инструкция касается всех сайтов с уязвимой версией модуля — в том числе тех, у кого закончился период бесплатных обновлений. Уязвимости позволяют неавторизованному или непривилегированному пользователю изменять настройки модуля и читать произвольные файлы сервера. Примените исправление как можно скорее.

Затронутые версии

МодульУязвимыИсправлено
corsik.yadelivery26.1.0 и ранеев следующем обновлении (см. Обновления)
corsik.suggestions26.1.0 и ранеев следующем обновлении (см. Обновления)

Проверить версию: Marketplace → Установленные решения, либо файл bitrix/modules/<ID модуля>/install/version.php.

Два способа устранения

  1. Обновление модуля (рекомендуется). Если период обновлений активен — установите ближайшее вышедшее обновление через Marketplace → Обновления. Все правки уже включены в него.
  2. Ручной патч (если обновление недоступно). Если период обновлений закончился, внесите изменения вручную по разделам ниже. Правки затрагивают только PHP-файлы модулей и безопасны для существующих данных.

Внимание

Перед ручной правкой сделайте резервную копию каталогов bitrix/modules/corsik.yadelivery/ и bitrix/modules/corsik.suggestions/. После правок очистите кеш Bitrix (Настройки → Настройки продукта → Автокеширование → Очистить файлы кеша).


Модуль corsik.yadelivery

1. Фильтр прав администратора (новый файл)

Создайте файл bitrix/modules/corsik.yadelivery/lib/controller/AdminAuthorizationFilter.php:

<?php

declare(strict_types=1);

namespace Corsik\YaDelivery\Controller;

use Bitrix\Main\Engine\ActionFilter\Base;
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\Error;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

final class AdminAuthorizationFilter extends Base
{
	public const ERROR_ACCESS_DENIED = 'CORSIK_YADELIVERY_ACCESS_DENIED';

	public function onBeforeAction(Event $event): ?EventResult
	{
		if (!CurrentUser::get()->isAdmin())
		{
			$this->addError(new Error(
				(string)Loc::getMessage(self::ERROR_ACCESS_DENIED),
				self::ERROR_ACCESS_DENIED,
			));

			return new EventResult(EventResult::ERROR, null, null, $this);
		}

		return null;
	}
}

И языковой файл bitrix/modules/corsik.yadelivery/lang/ru/lib/controller/AdminAuthorizationFilter.php:

<?php

$MESS['CORSIK_YADELIVERY_ACCESS_DENIED'] = 'Доступ запрещён: требуются права администратора.';

2. Подключение фильтра в админ-контроллерах

В перечисленных файлах в методе getDefaultPreFilters() добавьте Authentication и AdminAuthorizationFilter в начало массива фильтров.

Было:

protected function getDefaultPreFilters(): array
{
	return [
		new ActionFilter\Csrf(),
		new ActionFilter\CloseSession(),
		new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
	];
}

Стало:

protected function getDefaultPreFilters(): array
{
	return [
		new ActionFilter\Authentication(),
		new AdminAuthorizationFilter(),
		new ActionFilter\Csrf(),
		new ActionFilter\CloseSession(),
		new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
	];
}

Применить к файлам:

ФайлЧто защищает
lib/controller/AbstractSetupController.phpсохранение настроек доставки и модального окна
lib/controller/AdditionalFields.phpдополнительные поля доставки
lib/controller/suggestions.phpнастройки подсказок и DaData
lib/controller/dadata.phpобращения к платному API DaData
lib/controller/admin/rules.phpсоздание/изменение правил доставки
lib/controller/admin/geojson.phpразбор GeoJSON-файлов
lib/controller/admin/pages/AbstractEntityCrudController.phpCRUD складов и зон
lib/controller/EditPagesBanner.phpпереключение режима редакторов

Заметка

Для файлов в пространстве имён Corsik\YaDelivery\Controller\Admin (admin/rules.php, admin/geojson.php, admin/pages/AbstractEntityCrudController.php) добавьте импорт класса фильтра:

use Corsik\YaDelivery\Controller\AdminAuthorizationFilter;

Файлы из пространства Corsik\YaDelivery\Controller (AbstractSetupController.php, AdditionalFields.php, suggestions.php, dadata.php, EditPagesBanner.php) импорт не требуют — класс в том же пространстве имён.

Внимание

Не добавляйте эти фильтры в публичные checkout-контроллеры lib/controller/base.php (расчёт стоимости) и lib/controller/location.php (определение локации) — они вызываются покупателями на фронтенде и должны оставаться открытыми.

3. Path traversal в Core::getGeoJson()

В файле include.php, метод getGeoJson().

Было:

public static function getGeoJson(string $geoJson): array
{
	$file = new File(Application::getDocumentRoot() . $geoJson);

	return Helper::JsonDecode($file->getContents());
}

Стало:

public static function getGeoJson(string $geoJson): array
{
	$documentRoot = rtrim(Application::getDocumentRoot(), '/\\');
	$realPath = realpath($documentRoot . '/' . ltrim($geoJson, '/\\'));

	if (
		$realPath === false
		|| !str_starts_with($realPath, $documentRoot . DIRECTORY_SEPARATOR)
		|| !in_array(strtolower((string)pathinfo($realPath, PATHINFO_EXTENSION)), ['json', 'geojson'], true)
	)
	{
		return [];
	}

	return Helper::JsonDecode((new File($realPath))->getContents());
}

4. Неконтролируемая запись в файл по log_path

В файле lib/controller/location.php добавьте метод нормализации имени лог-файла и используйте его при записи.

Добавьте метод:

private function resolveLogFile(): string
{
	$fileName = basename(Options::getOptionByName("log_path"));

	if ($fileName === '' || !preg_match('/\.(txt|log)$/i', $fileName))
	{
		return 'location_logs.txt';
	}

	return $fileName;
}

Замените вызов записи в лог:

// Было:
Debug::writeToFile($post, "(" . date("d.m.Y H:i:s") . ") Location", Options::getOptionByName("log_path"));

// Стало:
Debug::writeToFile($post, "(" . date("d.m.Y H:i:s") . ") Location", $this->resolveLogFile());

5. Запись произвольных опций модуля

В файле lib/Services/Setup/SuggestionsSetupService.php, метод saveDadataPayerSettings() — добавьте белый список перед циклом сохранения и пропускайте значения вне списка.

Было:

foreach ($mappings as $mapping)
{
	$propertyId = (string)($mapping['propertyId'] ?? '');
	$dadataType = (string)($mapping['dadataType'] ?? 'N');

	if ($propertyId !== '')
	{
		$this->setOption($propertyId, $dadataType);

		if ($dadataType === 'ADDRESS')
		{
			$addressProps[] = $propertyId;
		}
	}
}

Стало:

$allowedPropertyIds = array_map(
	static fn(OrderProperty $property): string => (string)$property->id,
	$this->getOrderProperties($personTypeId),
);
$allowedDadataTypes = array_keys(DadataSuggestService::DaDataType());

foreach ($mappings as $mapping)
{
	$propertyId = (string)($mapping['propertyId'] ?? '');
	$dadataType = (string)($mapping['dadataType'] ?? 'N');

	if (
		!in_array($propertyId, $allowedPropertyIds, true)
		|| !in_array($dadataType, $allowedDadataTypes, true)
	)
	{
		continue;
	}

	$this->setOption($propertyId, $dadataType);

	if ($dadataType === 'ADDRESS')
	{
		$addressProps[] = $propertyId;
	}
}

6. XSS и CSRF на страницах списка/редактирования

В admin/pages/corsik_yadelivery_zone_edit.php оберните вывод сохранённых значений в htmlspecialcharsbx():

// COORDINATES
name="COORDINATES"><?= htmlspecialcharsbx($zone->coordinates) ?></textarea>

// KM
value="<?= htmlspecialcharsbx($rule->km) ?>" maxlength="255">

// PRICE
value="<?= htmlspecialcharsbx($rule->price) ?>" maxlength="255">

В admin/pages/corsik_yadelivery_warehouses.php:

// Экранирование имени склада в ссылке:
$row->AddViewField("NAME", '<a href="corsik_yadelivery_warehouse_edit.php?ID=' . $f_ID . '&lang=' . LANGUAGE_ID . '">' . htmlspecialcharsbx($f_NAME) . '</a>');

И проверка сессии перед групповыми операциями (внутри блока GroupAction):

if (!check_bitrix_sessid())
{
	$lAdmin->AddGroupError(GetMessage("MAIN_INVALID_SESSID"));
	$arID = [];
}

7. Отдельный AJAX-файл без авторизации

Если в вашей версии присутствует файл admin/pages/corsik_yadelivery_warehouse_ajax.php, он принимает запросы без проверки прав администратора. В исправленной версии его функциональность перенесена в защищённый контроллер. Если вы не обновляетесь, в начале файла (сразу после подключения пролога) добавьте проверку прав:

global $USER;
if (!isset($USER) || !$USER->IsAdmin())
{
	echo json_encode(['success' => false, 'errors' => ['Access denied']]);
	die();
}
if (!check_bitrix_sessid())
{
	echo json_encode(['success' => false, 'errors' => ['Invalid session']]);
	die();
}

Модуль corsik.suggestions

Админ-контроллеры модуля проверяли только факт авторизации (Authentication), но не права администратора. Это позволяло любому зарегистрированному пользователю (например, обычному покупателю) перезаписывать настройки модуля, включая глобальный ключ Яндекс.Карт сайта.

1. Фильтр прав администратора (новый файл)

Создайте файл bitrix/modules/corsik.suggestions/lib/Controller/AdminAuthorizationFilter.php:

<?php

declare(strict_types=1);

namespace Corsik\Suggestions\Controller;

use Bitrix\Main\Engine\ActionFilter\Base;
use Bitrix\Main\Engine\CurrentUser;
use Bitrix\Main\Error;
use Bitrix\Main\Event;
use Bitrix\Main\EventResult;
use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

final class AdminAuthorizationFilter extends Base
{
	public const ERROR_ACCESS_DENIED = 'CORSIK_SUGGESTIONS_ACCESS_DENIED';

	public function onBeforeAction(Event $event): ?EventResult
	{
		if (!CurrentUser::get()->isAdmin())
		{
			$this->addError(new Error(
				(string)Loc::getMessage(self::ERROR_ACCESS_DENIED),
				self::ERROR_ACCESS_DENIED,
			));

			return new EventResult(EventResult::ERROR, null, null, $this);
		}

		return null;
	}
}

И языковой файл bitrix/modules/corsik.suggestions/lang/ru/lib/Controller/AdminAuthorizationFilter.php:

<?php

$MESS['CORSIK_SUGGESTIONS_ACCESS_DENIED'] = 'Доступ запрещён: требуются права администратора.';

2. Подключение фильтра в админ-контроллерах

В файлах lib/Controller/Setup.php, lib/Controller/FormSuggestions.php, lib/Controller/DadataOrder.php добавьте AdminAuthorizationFilter сразу после Authentication.

Было:

return [
	new ActionFilter\Authentication(),
	new ActionFilter\Csrf(),
	new ActionFilter\CloseSession(),
	new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
];

Стало:

return [
	new ActionFilter\Authentication(),
	new AdminAuthorizationFilter(),
	new ActionFilter\Csrf(),
	new ActionFilter\CloseSession(),
	new ActionFilter\HttpMethod([ActionFilter\HttpMethod::METHOD_POST]),
];

Класс фильтра находится в том же пространстве имён Corsik\Suggestions\Controller, поэтому дополнительный use не требуется.

Внимание

Не добавляйте фильтр в публичные контроллеры lib/Controller/Base.php и lib/Controller/Location.php — они работают на фронтенде магазина.


Проверка после применения

  1. Очистите кеш Bitrix.
  2. Войдите в админку под администратором и убедитесь, что страницы настроек модулей открываются и сохранение работает.
  3. Откройте оформление заказа на фронтенде и убедитесь, что расчёт доставки и подсказки адреса работают (публичные эндпоинты не затронуты).
  4. Под пользователем без прав администратора (или в анонимной сессии) запросы к админским действиям должны возвращать ошибку доступа.

Подсказка

Если что-то перестало работать после ручной правки — восстановите файлы из резервной копии и обратитесь в техническую поддержку, приложив версию модуля и описание шагов.