Como encontramos uma vulnerabilidade crítica do AspNetCore.Mvc e mudamos para nossa própria serialização

Olá Habr!

Neste artigo, queremos compartilhar nossa experiência em otimizar o desempenho e explorar os recursos do AspNetCore.Mvc.



Antecedentes


Vários anos atrás, em um de nossos serviços carregados, notamos um consumo significativo de recursos da CPU. Parecia estranho, já que a tarefa do serviço era realmente pegar a mensagem e colocá-la na fila, tendo realizado anteriormente algumas operações, como validação, adição de dados, etc.

Como resultado da criação de perfil, descobrimos que a desserialização "consome" a maior parte do tempo do processador. Jogamos fora o serializador padrão e escrevemos por conta própria em Jil, como resultado do qual o consumo de recursos diminuiu várias vezes. Tudo funcionou como deveria e conseguimos esquecê-lo.

O problema


Estamos constantemente aprimorando nosso serviço em todas as áreas, incluindo segurança, desempenho e tolerância a falhas, para que a "equipe de segurança" realize vários testes de nossos serviços. E, há algum tempo, um alerta sobre um erro no registro "voa" para nós - de alguma forma, perdemos uma mensagem inválida por sua vez.

Com uma análise detalhada, tudo parecia bastante estranho. Existe um modelo de solicitação (aqui darei um exemplo de código simplificado):

public class RequestModel { public string Key { get; set; } FromBody] Required] public PostRequestModelBody Body { get; set; } } public class PostRequestModelBody { [Required] [MinLength(1)] public IEnumerable<long> ItemIds { get; set; } } 

Existe um controlador com a ação Post, por exemplo:

 [Route("api/[controller]")] public class HomeController : Controller { [HttpPost] public async Task<ActionResult> Post(RequestModel request) { if (this.ModelState.IsValid) { return this.Ok(); } return this.BadRequest(); } } 

Tudo parece lógico. Se uma solicitação vier da visualização Corpo

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

A API retornará BadRequest e há testes para isso.

No entanto, no log, vemos o oposto. Pegamos a mensagem dos logs, enviamos para a API e obtivemos o status OK ... e ... um novo erro no log. Não acreditando em nossos olhos, cometemos um erro e garantimos que sim, de fato, ModelState.IsValid == true. Ao mesmo tempo, eles notaram um tempo de execução de consulta incomumente longo de cerca de 500ms, enquanto o tempo de resposta usual raramente excede 50ms e isso está em produção, que atende a milhares de solicitações por segundo!

A diferença entre essa solicitação e nossos testes era apenas que ela continha mais de 600 linhas vazias ...

Em seguida, haverá muito código bukaf.

Razão


Eles começaram a entender o que estava errado. Para eliminar o erro, eles escreveram um aplicativo limpo (do qual dei um exemplo), com o qual reproduzimos a situação descrita. No total, passamos alguns dias-homem em pesquisas, testes, depuração mental do código AspNetCore.Mvc e verificamos que o problema estava no JsonInputFormatter .

JsonInputFormatter usa um JsonSerializer, que, obtendo um fluxo para desserialização e tipo, tenta serializar cada propriedade, se for uma matriz - todos os elementos dessa matriz. Ao mesmo tempo, se ocorrer um erro durante a serialização, o JsonInputFormatter salva cada erro e seu caminho, marca-o como processado, para que você possa continuar a tentativa de desserializar ainda mais.

Abaixo está o código para o manipulador de erros JsonInputFormatter (está disponível no Github no link acima):

 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; } 

Erros de marcação como processados ​​são cometidos no final do processador

 eventArgs.ErrorContext.Handled = true; 


Assim, um recurso é implementado para gerar todos os erros e caminhos de desserialização para eles, em quais campos / elementos eles estavam, bem ... quase todos ...

O fato é que o JsonSerializer tem um limite de 200 erros, após o qual ele falha, enquanto todo o código falha e o ModelState se torna ... válido! ... erros também são perdidos.

Solução


Sem hesitar, implementamos nosso formatador Json para o Asp.Net Core usando o Jil Deserializer. Como o número de erros no corpo não é absolutamente importante para nós e apenas o fato de sua presença é importante (e geralmente é difícil imaginar uma situação em que seria realmente útil), o código do serializador acabou sendo bastante simples.

Vou dar o código principal do JilJsonInputFormatter personalizado:

 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(); } 

Atenção! Jil faz distinção entre maiúsculas e minúsculas, significando o conteúdo do Corpo

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

e

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

não é a mesma coisa. No primeiro caso, se camelCase for usado, a propriedade ItemIds será nula após a desserialização.

Mas como essa é a nossa API, nós a usamos e controlamos; para nós, isso não é crítico. O problema pode surgir se for uma API pública e alguém já a chamar, passando o nome do parâmetro que não está no camelCase.

Resultado


O resultado até superou nossas expectativas; a API começou a devolver o BadRequest à solicitação solicitada e o fez muito rapidamente. Abaixo estão as capturas de tela de alguns de nossos gráficos, que mostram claramente as mudanças no tempo de resposta e na CPU, antes e depois da implantação.
Solicitar prazo de entrega:

imagem

Por volta das 16:00, houve uma implantação. Antes da implantação, o tempo de execução do p99 era de 30 a 57ms, após a implantação, eram de 9 a 15ms. (Você não pode prestar atenção aos picos repetidos das 18:00 - essa foi outra implantação)

É assim que o gráfico da CPU mudou:

imagem

Por esse motivo, trouxemos um problema para o Github, no momento em que este artigo foi escrito, foi sinalizado como um bug com o marco 3.0.0-preview3.

Em conclusão


Até que o problema seja resolvido, acreditamos que é melhor abandonar o uso da desserialização padrão, especialmente se você tiver uma API pública. Conhecendo esse problema, um invasor pode facilmente colocar seu serviço, lançando várias solicitações inválidas semelhantes, porque quanto maior a matriz incorreta, mais Body, mais tempo o processamento ocorre no JsonInputFormatter.

Artyom Astashkin, líder da equipe de desenvolvimento

Source: https://habr.com/ru/post/pt435626/


All Articles