25. TLV 옵션 리스트 역직렬화의 경계·길이 검증 부재
난이도 중해설 — 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.Copy가ArgumentException(소스 범위 초과)으로 실패. - 재현:
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>로 불필요한 중간 할당을 없애 증폭 표면을 줄이는 법.
해설 — TLV 옵션 리스트 역직렬화의 무한루프·언더플로·OOB (C++)
난이도: 중상
요약
엔트리 전체 길이 entryLen 을 남은 바이트와 대조하지 않고 신뢰한다.
세 가지 신뢰 불가 입력이 각각 치명적이다: entryLen == 0 이면 pos 가 전진하지
않아 무한루프(A), entryLen < 3 이면 entryLen - 3 이 size_t 로
언더플로해 거대한 길이가 되어 버퍼 밖을 대량 복사(B), 정상 형식이어도
헤더·value 가 남은 바이트를 넘으면 OOB 읽기(UB·정보 누출)(C).
문제점
(1) [무한루프/전진보장 부재] entryLen == 0 — (A)
- 증상:
pos += entryLen이 0 만큼 전진 → 같은 위치를 영원히 반복. CPU 100%, 워커가 한 패킷에 묶여 전체 수신 처리가 멈춤(DoS). - 재현:
entryLen = 0한 엔트리. - 근본원인: 루프가 매 반복 최소 전진(헤더 크기 이상) 을 보장하지 않음.
(2) [정수 언더플로 → 대량 OOB] entryLen < 3 — (B)
- 증상:
valLen = entryLen - 3가size_t무부호 연산이라0 - 3 == SIZE_MAX-2.assign(buf+3, buf+3+거대값)이 버퍼 밖을 읽어 크래시 또는 메모리 내용 유출. - 근본원인: 무부호 뺄셈 전
entryLen >= 3미검증.
(3) [경계검증 부재] 헤더·value OOB — (C)
- 증상: payload 끝에서 헤더 3바이트가 부족하거나,
entryLen이 남은 바이트보다 크면buf[pos+1],buf+pos+3+valLen가 버퍼 밖을 가리킴(UB). - 근본원인:
pos + 3 <= n,entryLen <= n - pos미검증.
C# 트윈은 OOB 가 예외로 바뀌어 "크래시" 수준이지만, C++ 는 조용한 메모리 오버리드(인접 세션 버퍼 내용이 value 로 새어 나갈 수 있음)와 무한루프 까지 가능해 더 위험하다.
수정안
매 단계 경계를 확인하고, 최소 전진과 길이 하한을 강제하며, 총량 상한을 둔다.
bool parse(const uint8_t* buf, size_t n, std::vector<Option>& out) {
size_t pos = 0, total = 0;
const size_t kMaxEntries = 64, kMaxTotal = 16 * 1024;
while (pos < n) {
if (n - pos < 3) return false; // (C') 헤더 경계
uint16_t entryLen =
static_cast<uint16_t>(buf[pos] | (buf[pos + 1] << 8));
if (entryLen < 3) return false; // (B') 하한 → 언더플로 차단
if (entryLen > n - pos) return false; // (C') 엔트리 경계
uint8_t type = buf[pos + 2];
size_t valLen = static_cast<size_t>(entryLen) - 3;
total += valLen;
if (out.size() >= kMaxEntries || total > kMaxTotal) // 총량 상한
return false;
Option o;
o.type = type;
o.value.assign(buf + pos + 3, buf + pos + 3 + valLen);
out.push_back(std::move(o));
pos += entryLen; // entryLen>=3 이 보장되어 매번 최소 3 전진
}
return true;
}
entryLen >= 3검사가 언더플로 차단과 최소 전진 보장을 동시에 해결한다.- 경계 위반은 UB 대신
false(거부)로 끝낸다.
더 나은 설계 (트레이드오프)
std::span<const uint8_t>(C++20) 또는 경계 동반 커서 타입으로 모든 읽기를 감싸 휴먼에러를 구조적으로 차단.- 무부호 뺄셈은 항상 하한 검사 후:
a - b전에a >= b확인을 습관화. - 알 수 없는 type 스킵도 반드시 entryLen 만큼 전진(스킵 경로의 0/언더플로 동일 주의).
면접 포인트
- "전진이 입력값에 의존"하면 반드시 최소 전진 하한을 둬야 무한루프를 막는다.
- 무부호 정수 뺄셈 언더플로는 길이 계산에서 가장 흔한 원격 취약점 —
len - hdr형태를 보면 즉시 의심하라. - C++ 의 조용한 OOB 읽기는 크래시보다 정보 누출이 더 큰 위협일 수 있다.