안전하지 않은 역직렬화

C#에서 신뢰할 수 없는 데이터의 안전하지 않은 역직렬화를 방지합니다.

설명

역직렬화는 바이트나 텍스트로 저장된 데이터를 런타임 객체로 복원하는 작업입니다. 입력 페이로드가 생성할 .NET 타입을 지정할 수 있으면, 공격자는 애플리케이션과 의존성에 존재하는 가젯 타입을 선택하여 생성자, 속성 설정자, 역직렬화 콜백 등의 부작용을 실행할 수 있습니다.

BinaryFormatter는 이러한 타입 정보를 페이로드에 포함하는 범용 객체 그래프 역직렬화기입니다. 바인더나 설정으로 안전하게 만들 수 없으며, .NET 9부터 런타임 내장 구현은 사용할 때 항상 PlatformNotSupportedException을 발생시킵니다. 별도의 지원되지 않는 호환성 패키지로 기능을 다시 활성화할 수 있지만 취약성도 함께 복원되므로 보안 해결책이 아닙니다. Microsoft는 SoapFormatter, NetDataContractSerializer, LosFormatter, ObjectStateFormatter도 제한 없는 다형성 역직렬화가 가능한 위험한 대안으로 분류합니다.

Json.NET의 TypeNameHandling은 별도의 경우입니다. 기본값인 TypeNameHandling.None은 입력의 $type 메타데이터를 타입 선택에 사용하지 않습니다. All, Auto, Objects, Arrays를 사용하면 입력이 타입 선택에 관여하므로, 외부 데이터를 역직렬화할 때는 명시적인 허용 목록을 강제하는 ISerializationBinder가 필요합니다. 바인더가 실제로 허용 목록을 강제하고 예상하지 않은 타입을 거부하는지 확인해야 합니다.

잠재적 영향

  • 애플리케이션 프로세스 권한으로 원격 코드 실행
  • 파일 읽기·쓰기, 네트워크 요청, 비밀정보 유출
  • 인증 또는 권한 검사 우회와 객체 상태 변조
  • 과도한 객체 그래프, 재귀, 할당을 이용한 서비스 거부

해결 방법

  1. BinaryFormatter와 제한 없는 레거시 포매터를 제거합니다. SerializationBinder를 추가하는 방식으로 BinaryFormatter를 유지하지 않습니다.
  2. 새 JSON 코드에는 명시적인 DTO를 대상으로 하는 System.Text.Json을 우선 사용합니다. 다형성이 필요하면 허용할 파생 타입과 판별자만 명시합니다.
  3. 기존 NRBF 데이터를 마이그레이션해야 하면 System.Formats.Nrbf.NrbfDecoder로 레코드를 읽고, 애플리케이션이 정의한 닫힌 타입 집합에 수동으로 매핑합니다. 페이로드가 지정한 타입을 로드하거나 인스턴스화하지 않습니다.
  4. Json.NET에서는 가능하면 TypeNameHandling.None을 유지합니다. 불가피한 경우 ISerializationBinder.BindToType이 예상하지 않은 모든 타입에 대해 null을 반환하거나 예외를 발생시키도록 구현하고, 역직렬화에 사용하는 설정에 직접 지정합니다.
  5. 직렬화 데이터가 신뢰 경계를 왕복한다면 HMAC이나 디지털 서명으로 무결성과 출처를 검증하고 키 교체를 설계합니다. 재전송 방지는 nonce, 순서 번호, 만료 시간 등의 별도 메커니즘으로 구현합니다. 이러한 검사는 타입 제한을 보완하지만 BinaryFormatter를 안전하게 만들지는 않습니다.

예시

변경 전

BinaryFormatter에 요청 본문 전달

csharp
using Microsoft.AspNetCore.Mvc;
using System.Runtime.Serialization.Formatters.Binary;

public object Parse([FromBody] Stream stream)
{
    var formatter = new BinaryFormatter();
    return formatter.Deserialize(stream);
}

바인더 없이 Json.NET 타입 메타데이터 허용

csharp
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

public object Parse([FromBody] string json)
{
    return JsonConvert.DeserializeObject(
        json,
        new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto
        });
}

변경 후

명시적인 DTO로 역직렬화

csharp
using System.Text.Json;

public sealed record PaymentRequest(string AccountId, decimal Amount);

PaymentRequest request =
    JsonSerializer.Deserialize<PaymentRequest>(json)
    ?? throw new JsonException("Missing request body");

허용한 파생 타입만 사용하는 System.Text.Json 다형성

csharp
using System.Text.Json;
using System.Text.Json.Serialization;

[JsonPolymorphic(TypeDiscriminatorPropertyName = "$kind")]
[JsonDerivedType(typeof(CardPayment), "card")]
public abstract class Payment;

public sealed class CardPayment : Payment;

Payment payment =
    JsonSerializer.Deserialize<Payment>(json)
    ?? throw new JsonException("Missing payment");

레거시 NRBF 페이로드를 객체 생성 없이 읽기

csharp
using System.Formats.Nrbf;

SerializationRecord root = NrbfDecoder.Decode(stream);

// root의 레코드를 크기·깊이 제한과 닫힌 타입 매핑으로 검증한 뒤
// 애플리케이션 DTO에 필요한 값만 복사합니다.

NrbfDecoder의 결과에서 임의 타입을 다시 로드하거나 범용 객체 그래프를 재구성하면 보호 효과가 사라집니다.

Json.NET에 제한적인 바인더 적용

csharp
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public sealed class KnownTypesBinder : ISerializationBinder
{
    public Type BindToType(string? assemblyName, string typeName) =>
        typeName == nameof(PaymentRequest)
            ? typeof(PaymentRequest)
            : throw new JsonSerializationException("Unexpected type");

    public void BindToName(
        Type serializedType,
        out string? assemblyName,
        out string? typeName)
    {
        if (serializedType != typeof(PaymentRequest))
        {
            throw new JsonSerializationException("Unexpected type");
        }

        assemblyName = null;
        typeName = nameof(PaymentRequest);
    }
}

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.Objects,
    SerializationBinder = new KnownTypesBinder()
};

PaymentRequest request =
    JsonConvert.DeserializeObject<PaymentRequest>(json, settings)
    ?? throw new JsonSerializationException("Missing request");

가능하면 이 호환 구성보다 TypeNameHandling.None과 명시적 DTO를 우선합니다. 바인더가 기본 타입 로더로 폴백하거나 입력의 어셈블리·타입 이름을 그대로 Type.GetType에 전달하면 허용 목록이 아닙니다.

분류

  • CWE-502: Deserialization of Untrusted Data
  • OWASP Top 10 2025 A08: Software or Data Integrity Failures
  • OWASP Top 10 2021 A08: Software and Data Integrity Failures
  • OWASP ASVS 5.0.0 v5.0.0-1.5.2: Safe Deserialization

참조