我们如何找到AspNetCore.Mvc的严重漏洞并切换到我们自己的序列化

哈Ha!

在本文中,我们想分享我们在优化性能和探索AspNetCore.Mvc功能方面的经验。



背景知识


几年前,在我们加载的一项服务中,我们注意到CPU资源的大量消耗。 看起来很奇怪,因为该服务的任务是实际上已经接收了消息并将其放入队列中,并且之前已经对该消息执行了一些操作,例如验证,数据添加等。

分析的结果是,我们发现反序列化“占用”了处理器的大部分时间。 我们抛出了标准的序列化器,并在Jil上编写了自己的序列化器,结果资源消耗减少了数倍。 一切都按其应有的方式工作,我们设法忘记了它。

问题


我们在安全,性能和容错性等各个方面都在不断改善我们的服务,因此“安全团队”对我们的服务进行各种测试。 不久前,关于日志错误的警报“飞”向我们-不知何故我们又错过了一条无效消息。

经过详细的分析,一切看起来都很奇怪。 有一个请求模型(这里我将给出一个简化的代码示例):

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

有一个带有操作Post的控制器,例如:

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

一切似乎合乎逻辑。 如果请求来自“正文”视图

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

该API将返回BadRequest,对此有测试。

然而,在日志中我们看到了相反的情况。 我们从日志中获取了消息,并将其发送到API,并获得状态OK ...和...日志中的新错误。 不相信我们的眼睛,我们犯了一个错误,并确保是的,确实是ModelState.IsValid == true。 同时,他们注意到异常长的查询执行时间约为500毫秒,而通常的响应时间很少超过50毫秒,并且正在生产中,每秒可处理数千个请求!

此请求与我们的测试之间的区别仅在于该请求包含600多个空行...

接下来将有很多bukaf代码。

原因


他们开始了解出什么问题了。 为了消除该错误,他们编写了一个干净的应用程序(从中给出了一个示例),我们使用该应用程序重现了所描述的情况。 总的来说,我们花了一些工夫研究,测试,心理调试代码AspNetCore.Mvc,结果发现问题出在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变为... valid!...错误也丢失了。

解决方案


我们毫不犹豫地使用Jil Deserializer为Asp.Net Core实现了Json格式化程序。 由于体内错误的数量对我们绝对不重要,因此仅存在错误的事实就很重要(通常很难想象这种情况确实有用),因此序列化程序代码非常简单。

我将给出自定义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(); } 

注意! 吉尔区分大小写,这意味着身体的内容

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



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

不一样的东西。 在第一种情况下,如果使用camelCase,则反序列化后的ItemIds属性将为null。

但是由于这是我们的API,所以我们可以使用和控制它,对我们而言这并不重要。 如果它是一个公共API,并且有人已经调用了它,并且没有在camelCase中传递参数名称,则可能会出现问题。

结果


结果甚至超出了我们的预期,API有望开始将BadRequest返回到所请求的请求,并且很快完成了请求。 下面是一些图表的屏幕截图,清楚地显示了部署前后的响应时间和CPU的变化。
要求提前期:

图片

大约在16:00进行了部署。 部署之前,p99的执行时间为30-57ms,部署之后为9-15ms。 (您不能注意18:00的重复高峰-这是另一个部署)

这是CPU图形的变化方式:

图片

因此,在撰写本文时,我们向Github带来了问题 ,它被标记为里程碑3.0.0-preview3的错误。

总结


在问题解决之前,我们认为最好不要使用标准反序列化,尤其是在拥有公共API的情况下。 知道了这个问题,攻击者可以轻松地将您的服务放入其中,并向其中抛出一堆类似的无效请求,因为错误数组越大,正文越多,则JsonInputFormatter中的处理时间就越长。

开发团队负责人Artyom Astashkin

Source: https://habr.com/ru/post/zh-CN435626/


All Articles