Aplikasi desktop C# warisan (legacy) seperti WinForms atau WPF sering memanfaatkan BinaryFormatter untuk menyimpan dan memuat save data, session, atau state game secara instan ke dalam bentuk stream biner. Saat arsitektur ini dimodernisasi menjadi Web API berbasis ASP.NET Core, membuka endpoint yang menerima binary payload mentah dari klien dapat membuka celah fatal: Remote Code Execution (RCE). BinaryFormatter secara mendasar tidak aman dan telah ditandai obsolete secara bertahap oleh Microsoft sejak .NET Core hingga dinonaktifkan secara default di rilis .NET modern.

Anatomi Celah Insecure Deserialization dengan BinaryFormatter

BinaryFormatter bekerja dengan menyertakan metadata tipe objek (type name dan assembly) langsung di dalam payload serialized. Saat payload didekodekan, deserializer secara otomatis menginisialisasi kelas yang dirujuk oleh data tanpa validasi awal apakah tipe tersebut aman atau relevan dengan state aplikasi.

Penyerang dapat memanfaatkan mekanisme ini menggunakan teknik gadget chains (misalnya via kelas yang mengimplementasikan ISerializable atau finalizer di library .NET bawaan) untuk memaksa runtime mengeksekusi kode arbitrary saat deserialisasi berlangsung, sebelum kode bisnis aplikasi sempat memvalidasi objek tersebut.

Kode Rentan Warisan Legacy

Contoh implementasi controller endpoint yang menerima payload biner mentah secara langsung:

// KODE VULNERABLE: JANGAN GUNAKAN DI PRODUCTION
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class LegacySaveController : ControllerBase
{
    [HttpPost("upload-save")]
    public IActionResult LoadSave([FromBody] byte[] rawStatePayload)
    {
        var formatter = new BinaryFormatter();
        using var stream = new MemoryStream(rawStatePayload);

        // Titik eksploitasi RCE: formatter mengeksekusi gadget chains di payload
        #pragma warning disable SYSLIB0011
        var gameState = (LegacyGameState)formatter.Deserialize(stream);
        #pragma warning restore SYSLIB0011

        return Ok(new { gameState.PlayerId, gameState.Score });
    }
}

Strategi Migrasi: Deserialisasi Aman Berbasis System.Text.Json

Pendekatan standar industri untuk mitigasi adalah beralih sepenuhnya ke payload data-only berbasis kontrak yang jelas menggunakan format JSON via System.Text.Json. Berbeda dengan formatter biner, parser JSON standar memetakan teks hanya ke tipe target yang sudah didefinisikan (strongly-typed), tanpa kemampuan instansiasi sembarang class dari metadata tipe bawaan.

Desain DTO dan Validasi State Ketat

Untuk mencegah manipulasi state game (seperti modifikasi parameter inventory, skor, level secara tidak wajar), kontrak data harus dipisah menjadi Data Transfer Object (DTO) dengan validasi deklaratif dan kontrol tipe statis.

using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

public sealed class SaveStatePayloadDto
{
    [JsonPropertyName("playerId")]
    [Required]
    [RegularExpression(@"^[a-zA-Z0-9_-]{8,32}$", ErrorMessage = "Format Player ID tidak valid")]
    public string PlayerId { get; set; } = string.Empty;

    [JsonPropertyName("level")]
    [Range(1, 100, ErrorMessage = "Level harus di antara 1 dan 100")]
    public int Level { get; set; }

    [JsonPropertyName("score")]
    [Range(0, 10_000_000, ErrorMessage = "Skor melampaui batas normal")]
    public long Score { get; set; }

    [JsonPropertyName("inventory")]
    [MaxLength(50, ErrorMessage = "Kapasitas inventory maksimum 50 slot")]
    public List<string> InventoryItems { get; set; } = new();
}

Implementasi Endpoint Web API yang Di-Hardening

Kode endpoint hasil refactoring mengisolasi payload, memastikan parsing JSON berjalan aman, dan memverifikasi integritas state data aplikasi secara logis sebelum disimpan ke database.

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/v1/saves")]
public class SafeSaveStateController : ControllerBase
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = false,
        MaxDepth = 8 // Mencegah stack overflow attack via deeply nested JSON
    };

    [HttpPost]
    [Consumes("application/json")]
    public IActionResult SaveGameState([FromBody] SaveStatePayloadDto payload)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Sanitasi tambahan: cegah duplikasi atau karakter berbahaya pada list
        var sanitizedItems = payload.InventoryItems
            .Where(item => !string.IsNullOrWhiteSpace(item) && item.Length <= 64)
            .Distinct()
            .ToList();

        // Operasi domain aman dengan data tervalidasi
        var updatedState = new
        {
            payload.PlayerId,
            payload.Level,
            payload.Score,
            Inventory = sanitizedItems,
            LastUpdated = DateTimeOffset.UtcNow
        };

        return Ok(updatedState);
    }
}

Mempertahankan Format Biner: Kapan Beralih ke Protobuf atau MessagePack?

Jika payload berukuran sangat masif dan memerlukan efisiensi serialization biner tinggi (misalnya real-time snapshot game), jangan kembali ke BinaryFormatter atau NetDataContractSerializer. Gunakan serializer berbasis schema eksplisit seperti Protocol Buffers (protobuf-net) atau MessagePack-CSharp.

Peringatan Keamanan: Saat menggunakan MessagePack-CSharp, pastikan tidak mengaktifkan TypelessContractlessStandardResolver tanpa kontrol keamanan, karena fitur tersebut meniru perilaku insecure type-handling seperti BinaryFormatter. Selalu gunakan strongly-typed resolver.

Checklist Migrasi dan Mitigasi

  1. Hapus BinaryFormatter: Cari seluruh penggunaan BinaryFormatter, NetDataContractSerializer, ObjectStateFormatter, dan SoapFormatter di seluruh solusi aplikasi.
  2. Tetapkan Batasan Payload: Pasang middleware atau atribut untuk membatasi ukuran request body ([RequestSizeLimit]) guna menghindari serangan Denial of Service (DoS).
  3. Validasi Input Otomatis: Manfaatkan System.ComponentModel.DataAnnotations atau FluentValidation untuk memastikan integritas state game atau save data.
  4. Kunci Deserializer Depth: Batasi MaxDepth pada JsonSerializerOptions untuk membatasi nesting depth JSON object.