Bagaimana kami menemukan kerentanan kritis AspNetCore.Mvc dan beralih ke serialisasi kami sendiri

Halo, Habr!

Pada artikel ini, kami ingin berbagi pengalaman kami dalam mengoptimalkan kinerja dan menjelajahi fitur-fitur AspNetCore.Mvc.



Latar belakang


Beberapa tahun yang lalu, di salah satu layanan kami yang dimuat, kami memperhatikan konsumsi sumber daya CPU yang signifikan. Itu tampak aneh, karena tugas layanan adalah untuk benar-benar mengambil pesan dan memasukkannya ke dalam antrian, setelah sebelumnya melakukan beberapa operasi di atasnya, seperti validasi, penambahan data, dll.

Sebagai hasil dari pembuatan profil, kami menemukan bahwa deserialisasi โ€œmemakanโ€ sebagian besar waktu prosesor. Kami membuang serializer standar dan menulis sendiri di Jil, akibatnya konsumsi sumber daya berkurang beberapa kali. Semuanya berjalan sebagaimana mestinya dan kami berhasil melupakannya.

Masalah


Kami terus meningkatkan layanan kami di semua bidang, termasuk keamanan, kinerja, dan toleransi kesalahan, sehingga "tim keamanan" melakukan berbagai tes layanan kami. Dan beberapa waktu yang lalu peringatan tentang kesalahan dalam log โ€œterbangโ€ kepada kami - entah bagaimana kami melewatkan pesan yang tidak valid pada gilirannya.

Dengan analisis terperinci, semuanya tampak agak aneh. Ada model-permintaan (di sini saya akan memberikan contoh kode yang disederhanakan):

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

Ada controller dengan action Post, misalnya:

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

Segalanya tampak logis. Jika permintaan datang dari tampilan Tubuh

 {"itemIds":["","","" โ€ฆ ] } 

API akan mengembalikan BadRequest, dan ada tes untuk ini.

Namun demikian, dalam log kita melihat yang sebaliknya. Kami mengambil pesan dari log, mengirimkannya ke API dan mendapat status OK ... dan ... kesalahan baru di log. Tidak mempercayai mata kami, kami melakukan kesalahan dan memastikan bahwa ya, memang ModelState.IsValid == benar. Pada saat yang sama, mereka melihat waktu eksekusi permintaan yang luar biasa panjang sekitar 500 ms, sementara waktu respons yang biasa jarang melebihi 50 ms dan ini dalam produksi, yang melayani ribuan permintaan per detik!

Perbedaan antara permintaan ini dan pengujian kami hanya bahwa permintaan tersebut berisi 600+ baris kosong ...

Selanjutnya akan banyak kode bukaf.

Alasan


Mereka mulai mengerti apa yang salah. Untuk menghilangkan kesalahan, mereka menulis aplikasi yang bersih (dari mana saya memberi contoh), yang dengannya kami mereproduksi situasi yang dijelaskan. Secara total, kami menghabiskan beberapa hari kerja untuk penelitian, tes, mental debug kode AspNetCore.Mvc dan ternyata masalahnya ada di JsonInputFormatter .

JsonInputFormatter menggunakan JsonSerializer, yang, mendapatkan aliran untuk deserialisasi dan jenis, mencoba membuat serial setiap properti, jika itu adalah array - setiap elemen dari array ini. Pada saat yang sama, jika kesalahan terjadi selama serialisasi, JsonInputFormatter menyimpan setiap kesalahan dan jalurnya, menandainya sebagai diproses, sehingga Anda dapat melanjutkan upaya deserialize lebih lanjut.

Di bawah ini adalah kode untuk penangan kesalahan JsonInputFormatter (tersedia di Github di tautan di atas):

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

Menandai kesalahan saat diproses dilakukan di bagian paling akhir prosesor

 eventArgs.ErrorContext.Handled = true; 


Jadi, sebuah fitur diterapkan untuk mengeluarkan semua kesalahan deserialisasi dan jalurnya, pada bidang / elemen mana mereka, yah ... hampir semua ...

Faktanya adalah bahwa JsonSerializer memiliki batas 200 kesalahan, setelah itu crash, sementara semua kode crash dan ModelState menjadi ... valid! ... kesalahan juga hilang.

Solusi


Tanpa ragu, kami mengimplementasikan formatter Json kami untuk Asp.Net Core menggunakan Jil Deserializer. Karena jumlah kesalahan dalam tubuh sama sekali tidak penting bagi kami, hanya fakta keberadaan mereka yang penting (dan umumnya sulit membayangkan situasi ketika itu akan sangat berguna), kode serializer ternyata cukup sederhana.

Saya akan memberikan kode utama custom 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(); } 

Perhatian! Jil peka huruf besar kecil, artinya isi Tubuh

 {"ItemIds":["","","" โ€ฆ ] } 

dan

 {"itemIds":["","","" โ€ฆ ] } 

bukan hal yang sama. Dalam kasus pertama, jika camelCase digunakan, properti ItemIds akan menjadi nol setelah deserialization.

Tetapi karena ini adalah API kami, kami menggunakan dan mengendalikannya, bagi kami itu tidak penting. Masalahnya mungkin muncul jika itu adalah API publik dan seseorang telah memanggilnya, melewati nama parameter yang tidak ada di camelCase.

Hasil


Hasilnya bahkan melebihi harapan kami, API diharapkan mulai mengembalikan BadRequest ke permintaan yang diminta dan melakukannya dengan sangat cepat. Di bawah ini adalah tangkapan layar dari beberapa grafik kami, yang dengan jelas menunjukkan perubahan dalam waktu respons dan CPU, sebelum dan sesudah penyebaran.
Minta waktu tunggu:

gambar

Sekitar pukul 16:00 ada penyebaran. Sebelum penyebaran, waktu eksekusi p99 adalah 30-57ms, setelah penyebaran menjadi 9-15ms. (Anda tidak dapat memperhatikan puncak berulang pukul 18:00 - ini adalah penyebaran lain)

Ini adalah bagaimana grafik CPU berubah:

gambar

Untuk alasan ini, kami membawa masalah ke Github, pada saat penulisan, masalah ini ditandai sebagai bug dengan tonggak sejarah 3.0.0-preview3.

Kesimpulannya


Sampai masalah teratasi, kami percaya bahwa lebih baik untuk mengabaikan penggunaan deserialisasi standar, terutama jika Anda memiliki API publik. Mengetahui masalah ini, penyerang dapat dengan mudah memasukkan layanan Anda dengan melemparkan sekelompok permintaan tidak valid yang serupa ke dalamnya, karena semakin besar array yang salah, semakin banyak Tubuh, semakin lama pemrosesan berlangsung di JsonInputFormatter.

Artyom Astashkin, Ketua Tim Pengembangan

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


All Articles