• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Разбираем уязвимость CVE-2020-36620

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
В этой заметке разберём уязвимость CVE-2020-36620 и посмотрим, как NuGet-пакет для конвертации string в enum может сделать C# приложение уязвимым к DoS-атакам.

1038_CVE_EnumStringValues_ru/image1.png

Представим ситуацию: есть серверное приложение, которое взаимодействует с пользователем. В одном из сценариев приложение получает от пользователя данные в строковом представлении и конвертирует их в элементы перечисления (string -> enum).

Для преобразования строки в элемент перечисления можно воспользоваться стандартными средствами .NET:
Код:
String colorStr = GetColorFromUser();
if (Enum.TryParse(colorStr, out ConsoleColor parsedColor))
{
  // Process value...
}
А можно найти какой-нибудь NuGet-пакет и попробовать сделать то же самое с его помощью. Например, EnumStringValues.

Дух авантюризма побеждает, поэтому для конвертации строк мы берём пакет EnumStringValues 4.0.0. Восклицательный знак напротив версии пакета только подогревает наш интерес...

Посмотрим, как может выглядеть код с использованием API пакета:
Код:
static void ChangeConsoleColor()
{
  String colorStr = GetColorFromUser();

  if (colorStr.TryParseStringValueToEnum<ConsoleColor>(
        out var parsedColor))
  {
    // Change console color...
  }
  else
  {
    // Error processing
  }
}
Что здесь происходит:
  • данные, поступающие от пользователя, записываются в переменную colorStr;
  • с помощью API библиотеки EnumStringValues строка конвертируется в экземпляр перечисления ConsoleColor;
  • если конвертация проходит успешно (then-ветвь), то меняется цвет консоли, с которой работает пользователь;
  • в противном случае (else-ветвь) выдаётся сообщение об ошибке.
Строки конвертируется, приложение работает. Казалось бы здорово... но не совсем. Оказывается, что приложение может вести себя так:

1038_CVE_EnumStringValues_ru/image2.png

Ах да, у нас же пакет с "восклицательным знаком"... Попробуем разобраться, откуда такое потребление памяти. В этом нам поможет такой код:
Код:
while (true)
{
  String valueToParse = ....;
  _ = valueToParse.TryParseStringValueToEnum<ConsoleColor>(
        out var parsedValue);
}
Код бесконечно парсит строки, используя библиотеку — ничего необычного. Если valueToParse будет принимать значения строковых представлений элементов ConsoleColor ("Black", "Red" и т. п.), приложение будет вести себя ожидаемо:

1038_CVE_EnumStringValues_ru/image3.png

Проблемы начнутся, если записывать в valueToParse уникальные строки. Например, так:
Код:
String valueToParse = Guid.NewGuid().ToString();
В таком случае приложение начинает неконтролируемо потреблять память.

1038_CVE_EnumStringValues_ru/image4.png

Попробуем разобраться, в чём дело. Для этого заглянем внутрь метода TryParseStringValueToEnum<T>:
Код:
public static bool
TryParseStringValueToEnum<TEnumType>(
  this string stringValue,
  out TEnumType parsedValue) where TEnumType : System.Enum
{
  if (stringValue == null)
  {
    throw new ArgumentNullException(nameof(stringValue),
                                    "Input string may not be null.");
  }

  var lowerStringValue = stringValue.ToLower();
  if (!Behaviour.UseCaching)
  {
    return TryParseStringValueToEnum_Uncached(lowerStringValue,
                                              out parsedValue);
  }

  return TryParseStringValueToEnum_ViaCache(lowerStringValue,
                                            out parsedValue);
}
Ага, интересно. Оказывается, что под капотом есть опция кэширования — Behaviour.UseCaching. Так как явно опцию кэширования мы не трогали, посмотрим на значение по умолчанию:
Код:
/// <summary>
/// Controls whether Caching should be used. Defaults to false.
/// </summary>
public static bool UseCaching
{
  get => useCaching;
  set { useCaching = value; if (value) { ResetCaches(); } }
}

private static bool useCaching = true;
Если верить комментарию к свойству, по умолчанию кэши выключены. На самом деле — включены (useCachingtrue).

Уже сейчас можно догадаться, в чём проблема. Но чтобы убедиться наверняка, мы опустимся в код глубже.

С полученными знаниями возвращаемся в метод TryParseStringValueToEnum. В зависимости от опции кэширования будет вызван один из двух методов — TryParseStringValueToEnum_Uncached или TryParseStringValueToEnum_ViaCache:
Код:
if (!Behaviour.UseCaching)
{
  return TryParseStringValueToEnum_Uncached(lowerStringValue,
                                            out parsedValue);
}

return TryParseStringValueToEnum_ViaCache(lowerStringValue,
                                          out parsedValue);
В нашем случае свойство UseCaching имеет значение true, исполнение переходит в метод TryParseStringValueToEnum_ViaCache. Ниже приведён его код, однако вникать в него не обязательно — дальше мы поэтапно разберём, как работает метод.
Код:
/// <remarks>
/// This is a little more complex than one might hope,
/// because we also need to cache the knowledge
/// of whether the parse succeeded or not.
/// We're doing that by storing `null`,
/// if the answer is 'No'. And decoding that, specifically.
/// </remarks>
private static bool
TryParseStringValueToEnum_ViaCache<TEnumType>(
  string lowerStringValue, out TEnumType parsedValue) where TEnumType
                                                        : System.Enum
{
  var enumTypeObject = typeof(TEnumType);

  var typeAppropriateDictionary
    = parsedEnumStringsDictionaryByType.GetOrAdd(
        enumTypeObject,
        (x) => new ConcurrentDictionary<string, Enum>());

  var cachedValue
    = typeAppropriateDictionary.GetOrAdd(
        lowerStringValue,
        (str) =>
        {
          var parseSucceededForDictionary =       
                TryParseStringValueToEnum_Uncached<TEnumType>(
                  lowerStringValue,
                  out var parsedValueForDictionary);

          return   parseSucceededForDictionary
                 ? (Enum) parsedValueForDictionary
                 : null;
        });

  if (cachedValue != null)
  {
    parsedValue = (TEnumType)cachedValue;
    return true;
  }
  else
  {
    parsedValue = default(TEnumType);
    return false;
  }
}
Разберём, что происходит в методе.
Код:
var enumTypeObject = typeof(TEnumType);

var typeAppropriateDictionary
  = parsedEnumStringsDictionaryByType.GetOrAdd(
      enumTypeObject,
      (x) => new ConcurrentDictionary<string, Enum>());
В словаре parsedEnumStringsDictionaryByType ключ — тип перечисления, с которым идёт работа, а значение — кэш строк и результатов их парсинга.

Получается такая схема кэшей:

Кэш <Тип перечисления -> Кэш <Исходная строка -> Результат парсинга>>

parsedEnumStringsDictionaryByType
— статическое поле:
Код:
private static
ConcurrentDictionary<Type, ConcurrentDictionary<string, Enum>>
parsedEnumStringsDictionaryByType;
Таким образом, в typeAppropriateDictionary сохраняется ссылка на кэш значений для того типа перечисления, с которым идёт работа (enumTypeObject).

Дальше код парсит входную строку и сохраняет результат в typeAppropriateDictionary:
Код:
var cachedValue
  = typeAppropriateDictionary.GetOrAdd(lowerStringValue, (str) =>
    {
      var parseSucceededForDictionary
        = TryParseStringValueToEnum_Uncached<TEnumType>(
            lowerStringValue,
            out var parsedValueForDictionary);

      return   parseSucceededForDictionary
             ? (Enum) parsedValueForDictionary
             : null;
    });
В конце метод просто возвращает флаг успешности операции и записывает результирующее значение в out-параметр:
Код:
if (cachedValue != null)
{
  parsedValue = (TEnumType)cachedValue;
  return true;
}
else
{
  parsedValue = default(TEnumType);
  return false;
}
Ключевая проблема описана в комментарии к методу: This is a little more complex than one might hope, because we also need to cache the knowledge of whether the parse succeeded or not. We're doing that by storing `null', if the answer is 'No'. And decoding that, specifically.

Даже если входную строку распарсить не удалось, она всё равно сохранится в кэш typeAppropriateDictionary: в качестве результата парсинга будет записано значение null. Так как typeAppropriateDictionary — ссылка из словаря parsedEnumStringsDictionaryByType, хранимого статически, объекты живут между вызовами метода (что логично — на то они и кэши).

Получается вот что. Если злоумышленник может отправлять приложению уникальные строки, которые парсятся с помощью API библиотеки, у него есть возможность "заспамить" кэш со всеми вытекающими.

1038_CVE_EnumStringValues_ru/image5.png

Парсинг уникальных строк приведёт к разрастанию словаря typeAppropriateDictionary. "Заспамливание" кэша подтверждает отладчик:

1038_CVE_EnumStringValues_ru/image6.png

Проблема, которую мы только что разобрали, — уязвимость CVE-2020-36620. Дополнительная информация:
Фикс простой — парсинг входных значений убрали как таковой (ссылка на коммит).

Раньше typeAppropriateDictionary заполнялся по мере поступления данных:
  • входная строка — "Yellow", в кэш записывается пара { "yellow", ConsoleColor.Yellow };
  • входная строка — "Unknown", в кэш записывается пара { "unknown", null }
  • и т. д.
Теперь typeAppropriateDictionary заполняется заранее. Словарь изначально хранит отношения строковых представлений элементов перечисления к фактическим значениям:

1038_CVE_EnumStringValues_ru/image7.png

Входные значения в словарь не записываются — их только пытаются извлекать:
Код:
if (typeAppropriateDictionary.TryGetValue(lowerStringValue,
                                          out var cachedValue))
  ....
После этой правки кэш перестал быть уязвим к засорению уникальными строками.

Библиотека версии 4.0.1 уже включает фикс проблемы, однако соответствующий NuGet-пакет размечен как уязвимый. Судя по всему, информация берётся из GitHub Advisory, где и написано, что безопасная версия — 4.0.2. Однако в той же записи есть ссылки на данные из NVD и vuldb, где указано, что пакет безопасен уже с версии 4.0.1, а не 4.0.2. Путаница, в общем.

Ещё интересно вот что: в коде уязвимость была закрыта в конце мая 2019, а в базах информация о ней появилась спустя 3.5 года — в конце декабря 2022.

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

С одной стороны, такая задержка понятна — у проекта 3 форка и 16 звёзд, можно отнести его к категории "личных". С другой стороны, 200К загрузок пакета в сумме — это всё-таки 200К загрузок.

автор Сергей Васильев
источник pvs-studio.ru
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх