» » Как мы нашли критичную уязвимость AspNetCore.Mvc и перешли на собственную сериализацию

 

Как мы нашли критичную уязвимость AspNetCore.Mvc и перешли на собственную сериализацию

Автор: admin от 10-01-2019, 16:20, посмотрело: 16

Привет, Хабр!



В этой статье мы хотим поделиться нашим опытом в оптимизации производительности и исследовании особенностей AspNetCore.Mvc.



Как мы нашли критичную уязвимость AspNetCore.Mvc и перешли на собственную сериализацию


Предыстория



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



В результате профилирования мы обнаружили, что большую часть процессорного времени “съедает” десериализация. Мы выкинули стандартный сериализатор и написали свой на Jil, в результате чего потребление ресурсов снизилось в разы. Все работало как нужно и мы успели об этом позабыть.

JsonInputFormatter.



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



Приведу ниже код обработчика ошибок JsonInputFormatter (он доступен на Github по ссылке выше):




void ErrorHandler(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs eventArgs)
{
    successful = false;
    // When ErrorContext.Path does not include ErrorContext.Member, add Member to form full path.
    var path = eventArgs.ErrorContext.Path;
    var member = eventArgs.ErrorContext.Member?.ToString();
    var addMember = !string.IsNullOrEmpty(member);
    if (addMember)
    {
        // Path.Member case (path.Length < member.Length) needs no further checks.
        if (path.Length == member.Length)
        {
            // Add Member in Path.Memb case but not for Path.Path.
            addMember = !string.Equals(path, member, StringComparison.Ordinal);
        }
        else if (path.Length > member.Length)
        {
            // Finally, check whether Path already ends with Member.
            if (member[0] == '[')
            {
                addMember = !path.EndsWith(member, StringComparison.Ordinal);
            }
            else
            {
                addMember = !path.EndsWith("." + member, StringComparison.Ordinal);
            }
        }
    }


    if (addMember)
    {
        path = ModelNames.CreatePropertyModelName(path, member);
    }


    // Handle path combinations such as ""+"Property", "Parent"+"Property", or "Parent"+"[12]".
    var key = ModelNames.CreatePropertyModelName(context.ModelName, path);


    exception = eventArgs.ErrorContext.Error;

    var metadata = GetPathMetadata(context.Metadata, path);
    var modelStateException = WrapExceptionForModelState(exception);
    context.ModelState.TryAddModelError(key, modelStateException, metadata);

    _logger.JsonInputException(exception);


    // Error must always be marked as handled
    // Failure to do so can cause the exception to be rethrown at every recursive level and
    // overflow the stack for x64 CLR processes
    eventArgs.ErrorContext.Handled = true;
}


Пометка ошибки, как обработанной производится в самом конце обработчика



eventArgs.ErrorContext.Handled = true; 




Таким образом реализована фича вывода всех ошибок десериализации и путей к ним, на каких полях/элементах они были, ну… почти всех…



Дело в том, что у JsonSerializer есть ограничение на 200 ошибок, после которого он падает, при этом падает весь код и ModelState становится… валидной!… теряются и ошибки.



Решение



Не долго думая, мы реализовали свой форматтер Json для Asp.Net Core с использованием Jil Deserializer. Поскольку для нас абсолютно не важно количество ошибок в body, а важен лишь факт их наличия (да и в целом сложно представить себе ситуацию, когда это было бы действительно полезно), то код сериализатора получился достаточно простым.



Приведу основной код кастомного JilJsonInputFormatter:



using (var reader = context.ReaderFactory(request.Body, encoding))
{
    try
    {
        var result = JSON.Deserialize(
            reader: reader,
            type: context.ModelType,
            options: this.jilOptions);

        if (result == null && !context.TreatEmptyInputAsDefaultValue)
        {
            return await InputFormatterResult.NoValueAsync();
        }
        else
        {
            return await InputFormatterResult.SuccessAsync(result);
        }
    }
    catch
    {
        // какая-то обработка ошибок
    }

    return await InputFormatterResult.FailureAsync();
}


Внимание! Jil чувствителен к регистру, это значит, что содержимое Body



{"ItemIds":["","","" … ] }


и



{"itemIds":["","","" … ] } 


не одно и тоже. В первом случае, если используется camelCase в свойство ItemIds после десериализации будет null.



Но так как это наш API, мы используем и контролируем его, для нас это не критично. Проблема может возникнуть, если это публичный API и кто-то уже вызывает его, передавая имя параметров не в camelCase.



Результат



Результат даже превзошел наши ожидания, API ожидаемо стал возвращать на искомый запрос BadRequest и делать это очень быстро. Ниже приведу скрины некоторых наших графиков, на которых хорошо видны изменения времени ответа и CPU, до и после деплоя.

Время выполнения запроса:



Как мы нашли критичную уязвимость AspNetCore.Mvc и перешли на собственную сериализацию



Примерно в 16:00 был деплой. До деплоя время выполнения p99 составляло 30-57ms, после деплоя стало 9-15ms. (На повторные пики 18:00 можно не обращать внимания — это был уже другой деплой)



Вот так изменился график CPU:



Как мы нашли критичную уязвимость AspNetCore.Mvc и перешли на собственную сериализацию



По этому поводу мы завели issue в Github, на момент написания статьи она была помечена как bug с milestone 3.0.0-preview3.



В заключение



Пока проблема не решена, мы считаем что лучше отказаться от использования стандартной десериализации, особенно, если у вас публичный API. Зная эту проблему, злоумышленник может легко положить ваш сервис, закинув в него кучу подобных невалидных запросов, ведь чем больше ошибочный массив, чем больше Body, тем дольше происходит обработка в JsonInputFormatter.



Артем Асташкин, Руководитель группы разработки

Источник: Хабр / Интересные публикации

Категория: Программирование

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *