← 문제로

46. 널 종료 문자열 필드 파싱의 프레임 경계 미준수 (오배열·프레임 오염, C#)

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

해설 — 널 종료 문자열 필드 파싱의 프레임 경계 미준수 (오배열·프레임 오염, C#)

난이도: 중

요약

Array.IndexOf(buffer, 0, offset)가 프레임 끝(offset+frameLen)을 상한으로 두지 않고 세션 수신 링버퍼 전체에서 널 바이트를 찾는다(A). 계정명에 널 종료가 없으면 스캔이 프레임을 넘어 다음 프레임의 바이트에서 0을 만나고, 그 위치를 문자열 끝으로 삼는다. 그러면 계정명이 다음 메시지 내용을 포함해 부풀고, 이어지는 버전 읽기(B)는 완전히 다른 프레임의 바이트를 읽어 파싱이 어긋난다. 링버퍼에 0이 전혀 없으면 IndexOf가 -1을 반환해 nameLen이 음수가 되고 GetString이 ArgumentOutOfRangeException으로 세션을 죽인다.

문제점

  • 분류태그: 경계 검증 부재 / 신뢰 경계 침범(untrusted length) / 프레임 경계 오정렬
  • 증상: 특정 클라(널 종료 누락)에서 계정명이 비정상적으로 길거나 옆 유저의 데이터가 섞여 보이고, 버전 판정이 틀리며, 최악에는 예외로 세션 강제 종료(DoS).
  • 재현조건: 계정명 필드에 \0을 넣지 않고 프레임을 채워 전송. 뒤에 다른 프레임이 쌓여 있으면 정보 유출/오배달, 없으면 예외.
  • 근본원인: 널 스캔의 상한을 프레임 길이로 제한하지 않음. "널 종료가 반드시 있다"는 신뢰할 수 없는 가정.

수정안

스캔 범위를 프레임 안으로 한정하고, 종료 바이트가 없으면 잘못된 프레임으로 간주해 거부한다.

public static (string account, ushort version) Parse(byte[] buffer, int offset, int frameLen)
{
    int end = offset + frameLen;
    int nul = Array.IndexOf(buffer, (byte)0, offset, frameLen); // 범위 한정
    if (nul < 0) throw new ProtocolException("account not null-terminated");

    int nameLen = nul - offset;
    if (nameLen > MaxAccountLen) throw new ProtocolException("account too long");

    int p = nul + 1;
    if (p + 2 > end) throw new ProtocolException("truncated version");

    string account = Encoding.UTF8.GetString(buffer, offset, nameLen);
    ushort version = (ushort)(buffer[p] | (buffer[p + 1] << 8));
    return (account, version);
}

Array.IndexOf(buffer, value, startIndex, count) 오버로드로 count를 프레임 길이로 제한하는 것이 핵심이고, 이어지는 모든 읽기도 end를 상한으로 검증한다.

더 나은 설계

  • 바이너리 프로토콜에서 **널 종료 문자열을 피하고 길이 프리픽스(length-prefixed)**를 쓰는 것이 정석이다. 길이를 먼저 읽고 len이 남은 프레임 안인지 검증하면 스캔 자체가 필요 없다. 트레이드오프: 스키마 변경.
  • 파서를 "남은 바이트 수를 들고 다니는 SpanReader"로 감싸 모든 읽기가 자동으로 경계 검사되게 한다. 트레이드오프: 약간의 추상화 비용.

면접 포인트

  • 널 종료 문자열이 바이너리 프로토콜에서 위험한 이유(경계·인젝션·오배열)와 길이 프리픽스의 장점.
  • 신뢰할 수 없는 입력을 파싱할 때 "종료 바이트/길이가 항상 유효하다"는 가정이 만드는 취약점 계열(OOB read, 정보 유출, DoS).