← 문제로

25. TLV 옵션 리스트 역직렬화의 경계·길이 검증 부재

난이도 중
내 리뷰 · C#
해설 · C#

해설 — TLV 옵션 리스트 역직렬화의 경계·길이 검증 부재

난이도: 중상

요약

TLV(Type-Length-Value) 엔트리를 순회하면서 남은 바이트와 대조하지 않고 길이 헤더를 읽고(A), 신뢰 불가 len 으로 버퍼를 할당하며(C), len 바이트를 복사한다(B). 잘리거나 조작된 payload 가 오면 IndexOutOfRangeException / ArgumentException 으로 수신 워커가 예외를 던지고, 다수 엔트리·큰 len 으로 메모리 증폭(DoS) 이 일어난다.

문제점

(1) [경계검증 부재] 헤더 읽기 OOB — (A)

  • 증상: payload 마지막에 type 바이트만 있고 길이 2바이트가 없으면 buf[pos+1]/buf[pos+2] 에서 IndexOutOfRangeException.
  • 재현: payload 끝을 1~2바이트 부족하게 보낸다.
  • 근본원인: pos + 3 <= buf.Length 를 확인하지 않고 헤더 3바이트를 읽음.

(2) [경계검증 부재] value 복사 OOB — (B)

  • 증상: len 이 남은 바이트보다 크면 Array.CopyArgumentException(소스 범위 초과)으로 실패.
  • 재현: len = 0xFFFF 인데 뒤 바이트가 몇 개 없음.
  • 근본원인: pos + len <= buf.Length 미검증.

C# 의 관리 배열은 OOB 를 즉시 예외로 바꾸므로 C/C++ 처럼 메모리를 몰래 읽지는 않는다. 그러나 이 예외가 수신 루프에서 잡히지 않으면 워커 스레드/연결이 죽고, 광범위 catch 로 삼키면 부분 적용 상태가 남는다. 어느 쪽이든 한 악성 패킷이 정상 처리를 망가뜨린다.

(3) [자원 증폭] 신뢰 불가 len 선할당 — (C)

  • 증상: 작은 입력으로도 엔트리마다 최대 64KB 버퍼를 다수 할당. 엔트리 개수에 상한이 없어 GC 압박·메모리 폭증.
  • 근본원인: 할당 전 len 의 타당성(남은 바이트·총량 상한)을 안 본다.

수정안

매 단계 남은 바이트와 대조하고, 총 엔트리 수·누적 크기에 상한을 둔다.

public bool TryParse(ReadOnlySpan<byte> buf, out List<(byte, byte[])> result)
{
    result = new List<(byte, byte[])>();
    int pos = 0, total = 0;
    const int MaxEntries = 64, MaxTotal = 16 * 1024;

    while (pos < buf.Length)
    {
        if (pos + 3 > buf.Length) return false;            // (A') 헤더 경계
        byte type = buf[pos];
        int len = buf[pos + 1] | (buf[pos + 2] << 8);
        pos += 3;

        if (len > buf.Length - pos) return false;          // (B') value 경계
        total += len;
        if (result.Count >= MaxEntries || total > MaxTotal) // (C') 총량 상한
            return false;

        var value = buf.Slice(pos, len).ToArray();
        pos += len;
        result.Add((type, value));
    }
    return true;
}
  • 경계 위반은 예외가 아니라 false(거부) 로 처리해 수신 루프가 살아남게 한다.
  • ReadOnlySpan<byte> 로 헤더는 복사 없이 읽고, value 만 필요 시 복사한다.

더 나은 설계 (트레이드오프)

  • 선검증 후 처리: 1차 패스로 전체 TLV 구조의 유효성(경계·총량)만 검사하고, 2차 패스에서 적용. 부분 적용을 원천 차단(메모리 두 번 순회 비용).
  • 알 수 없는 type 정책: 모르는 type 은 안전히 건너뛰되, 반드시 len 만큼 전진시켜야 무한루프/오프셋 어긋남을 막는다(스킵도 경계 검증 대상).
  • 스키마 버전 게이트: 허용 type 화이트리스트 + 버전별 최대 크기 테이블.

면접 포인트

  • 신뢰 불가 입력 파싱의 3대 검증: 헤더 경계 / 값 경계 / 총량(개수·누적) 상한.
  • 관리 언어의 OOB 는 "메모리 안전"하지만 "가용성 안전"은 아니다 — 예외가 곧 DoS 가 될 수 있으므로 잘린 입력은 예외가 아닌 정상적 거부로 다뤄야 한다.
  • Span<T> 로 불필요한 중간 할당을 없애 증폭 표면을 줄이는 법.