← 문제로

35. 길이 필드 부호 혼동(signed/unsigned)으로 인한 음수 길이 처리 (C#)

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

해설 — 길이 필드 부호 혼동(signed/unsigned)으로 인한 음수 길이 처리 (C#)

난이도: 하

요약

프로토콜은 길이를 "부호 없는 16비트"로 정의했지만, 코드는 (A)에서 short(부호 있는 16비트)로 읽는다. 0x8000 이상의 길이(32768~65535)는 음수 short로 해석되고, 이 음수가 (B)의 검증과 배열 할당으로 흘러 들어가 검증을 우회하거나 예외를 던진다.

문제점

  • [역직렬화/부호 혼동] 부호 없는 길이를 signed로 해석. (short)((buf[off]<<8)|buf[off+1])는 상위 비트가 1이면 음수가 된다. 예: 바이트 0xFF 0xFF → 의도한 값 65535가 아니라 -1.
  • [경계 검증 우회] 음수 길이가 검증을 통과. (B)의 off + len > buf.Length에서 len이 음수면 좌변이 작아져 검증을 통과한다. 이어 new byte[len]은 음수 크기라 OverflowException (또는 ArgumentOutOfRangeException)을 던진다. 신뢰 못 할 입력 한 방에 서버 스레드가 예외로 떨어진다(DoS). 최소한 "정상 거부"여야 할 입력이 예외 경로로 샌다.
  • [경계 검증 부재] fieldCount·off 사전 검증 부족. buf[off++], buf[off], buf[off+1] 접근 전에 남은 바이트 수를 확인하지 않아, 짧은 패킷이면 IndexOutOfRangeException이 난다.

근본 원인

와이어 상의 "부호 없는" 필드를 언어의 부호 있는 타입으로 담으면서 명시적 마스킹을 하지 않았다. 바이트 → 정수 변환에서 부호 확장(sign extension)이 조용히 일어난다.

수정안

길이를 부호 없는 값으로 읽고(& 0xFFFF 또는 ushort), 모든 인덱스 접근 전에 경계를 검사한다. 검증 실패는 예외가 아니라 "잘못된 패킷" 거부로 처리한다.

public static bool TryParseFields(byte[] buf, int off, out List<byte[]> fields)
{
    fields = new List<byte[]>();
    if (off < 0 || off >= buf.Length) return false;
    int fieldCount = buf[off++];

    for (int i = 0; i < fieldCount; i++)
    {
        if (off + 2 > buf.Length) return false;                 // 길이 필드 경계
        int len = ((buf[off] << 8) | buf[off + 1]) & 0xFFFF;    // 부호 없는 16비트로 마스킹
        off += 2;
        if (len > buf.Length - off) return false;               // 오버플로 없는 비교
        var data = new byte[len];
        Buffer.BlockCopy(buf, off, data, 0, len);
        off += len;
        fields.Add(data);
    }
    return true;
}
  • & 0xFFFF로 부호 확장을 차단한다. off + len > buf.Length 대신 len > buf.Length - off처럼 좌변을 키우지 않는 비교를 쓰면 정수 오버플로도 함께 피한다.
  • 실패를 bool 반환(또는 결과 코드)으로 바꿔 신뢰 못 할 입력이 예외로 번지지 않게 한다.

더 나은 설계

  • BinaryPrimitives.ReadUInt16BigEndian(span) 같은 표준 API로 엔디안·부호를 명확히 한다.
  • Span<byte> 기반 리더로 오프셋·남은 길이를 캡슐화하면 매 접근마다 경계 검사를 강제할 수 있다.
  • 필드 총개수·총 크기에 상한을 두어 메모리 증폭(수천 개 필드)도 방어한다.

면접 포인트

  • 부호 확장이 언제 일어나는가 — 좁은 정수에서 넓은 signed로 변환할 때 최상위 비트가 부호로 해석된다.
  • 경계 비교에서 a + b > limit가 왜 위험한가(오버플로) — a > limit - b로 재작성.
  • 신뢰 경계(untrusted input)에서 "예외로 죽기" vs "정상 거부"의 차이와 DoS 관점.