كيف وجدنا ثغرة أمنية حرجة في AspNetCore.Mvc وتحولنا إلى تسلسلنا الخاص

مرحبا يا هبر!

في هذه المقالة ، نريد مشاركة تجربتنا في تحسين الأداء واستكشاف ميزات AspNetCore.Mvc.



الخلفية


منذ عدة سنوات ، في إحدى خدماتنا المحملة ، لاحظنا استهلاكًا كبيرًا لموارد وحدة المعالجة المركزية. بدا الأمر غريبًا ، نظرًا لأن مهمة الخدمة كانت في الواقع أخذ الرسالة ووضعها في قائمة الانتظار ، بعد إجراء بعض العمليات عليها مسبقًا ، مثل التحقق من الصحة ، إضافة البيانات ، إلخ.

نتيجة للتوصيف ، وجدنا أن إزالة التسلسل "تأكل" معظم وقت المعالج. طرحنا مسلسلًا قياسيًا وكتبنا كتابنا على 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; } } 

هناك وحدة تحكم مع إجراء نشر ، على سبيل المثال:

 [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 ، وهناك اختبارات لهذا.

ومع ذلك ، في السجل نرى عكس ذلك. أخذنا الرسالة من السجلات ، وأرسلناها إلى واجهة برمجة التطبيقات وحصلنا على الحالة "موافق" ... و ... خطأ جديد في السجل. لا تصدق أعيننا ، لقد ارتكبنا خطأ وتأكدنا من أن نعم ، بالفعل ModelState.IsValid == صحيح. في الوقت نفسه ، لاحظوا وقت تنفيذ استعلام طويل بشكل غير عادي يبلغ حوالي 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 ... صحيح! ... الأخطاء أيضًا مفقودة.

الحل


وبدون تردد ، قمنا بتطبيق منسق Json الخاص بنا على Asp.Net Core باستخدام Jil Deserializer. نظرًا لأن عدد الأخطاء في الجسم ليس مهمًا على الإطلاق بالنسبة إلينا ، فقط حقيقة وجودهم أمر مهم (ومن الصعب عمومًا تخيل موقف عندما يكون مفيدًا بالفعل) ، فقد تبين أن رمز المتسلسل بسيط للغاية.

سأقدم الرمز الرئيسي المخصص 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 خالية بعد إلغاء التسلسل.

ولكن نظرًا لأن هذا هو API الخاص بنا ، فإننا نستخدمه ونتحكم فيه ، لأنه ليس بالأمر الحاسم. قد تنشأ المشكلة إذا كانت واجهة برمجة تطبيقات عامة وشخص ما يسميها بالفعل ، ويمر اسم المعلمة وليس في camelCase.

النتيجة


النتيجة تجاوزت توقعاتنا ، حيث من المتوقع أن تبدأ API في إرجاع BadRequest إلى الطلب المطلوب وفعلت ذلك بسرعة كبيرة. فيما يلي لقطات لبعض الرسوم البيانية الخاصة بنا ، والتي تظهر بوضوح التغييرات في زمن الاستجابة ووحدة المعالجة المركزية ، قبل وبعد النشر.
طلب مهلة:

الصورة

في حوالي الساعة 16:00 كان هناك نشر. قبل النشر ، كان وقت تنفيذ p99 30-57ms ، بعد النشر أصبح 9-15ms. (لا يمكنك الانتباه إلى القمم المتكررة 18:00 - كان هذا نشر آخر)

هذه هي الطريقة التي تغير الرسم البياني وحدة المعالجة المركزية:

الصورة

لهذا السبب ، قمنا بإحضار المشكلة إلى جيثب ، في وقت كتابة هذا التقرير ، تم وضع علامة عليه كخطأ ذي معلم رئيسي 3.0.0 - معاينة 3.

في الختام


حتى يتم حل المشكلة ، نعتقد أنه من الأفضل التخلي عن استخدام إلغاء التسلسل القياسي ، خاصةً إذا كان لديك واجهة برمجة تطبيقات عامة. معرفة هذه المشكلة ، يمكن للمهاجم وضع خدمتك بسهولة ، مع طرح مجموعة من الطلبات غير الصالحة المماثلة فيها ، لأنه كلما زاد حجم المصفوفة الخاطئة ، زاد النص ، كلما طالت المعالجة في JsonInputFormatter.

أرتيوم أستاشكين ، قائد فريق التطوير

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


All Articles