← 문제로

19. 압축 패킷 해제 시 압축 폭탄(decompression bomb) 방어 (C#)

난이도 최상
내 리뷰 · C#
해설 · C#

해설 — 압축 패킷 해제 시 압축 폭탄(decompression bomb) 방어 (C#)

난이도: 최상

답변 프레임워크: 요약 → 문제 분류 → 원인 → 수정안 → 더 나은 설계

요약

DecompressPacket공격자 제어 값과 무한 팽창을 모두 신뢰한다. (A) new byte[originalLen] — 선언 originalLen(최대 ~4GB)으로 즉시 선할당 → 16바이트 패킷으로 OOM(over-allocation DoS). 게다가 이 버퍼는 쓰이지도 않는다. (B) inflate.CopyTo(outStream)출력 상한 없이 스트림 끝까지 전부 복사 → 작은 입력이 수 GB 로 팽창하는 압축 폭탄(ratio bomb) 에 그대로 당해 메모리·CPU 고갈. (C) 풀어낸 크기와 선언 originalLen일치 검증도, 압축비 상한도 없다. (D) compressedLen 이 실제 버퍼 경계 안인지 검사하지 않아 MemoryStream(buf, 8, compressedLen) 에서 ArgumentException/OOB 위험. 정답 한 줄: 선언 크기를 신뢰해 선할당하지 말고, "절대 출력 상한 + 압축비 상한" 을 두고 고정 청크로 읽으며 상한 초과 즉시 거부하고, compressedLen 경계와 produced==originalLen 을 검증한다.

변별: problem9(압축/암호화 협상·다운그레이드 폴백), problem17(비압축 길이 프리픽스 over-allocation/over-read)와 달리, 본 문제는 압축 해제 출력 팽창(ratio bomb)·선언 크기 신뢰 라는 압축 특유의 DoS 가 핵심이다.


문제점

(A) 선언 originalLen 으로 선할당 — over-allocation DoS (보안/리소스) ★간판

  • 분류 태그: trust-declared-size / resource-exhaustion.
  • 증상: new byte[originalLen] 는 검증 전에 originalLen 바이트를 할당한다. originalLen=0xFFFFFFFF 만 실어 보내면 실제 압축 데이터가 없어도 거대한 할당 시도 → OutOfMemoryException/프로세스 압박. 더구나 이 preallocated 버퍼는 사용되지도 않는다.
  • 재현조건: 큰 originalLen 패킷 1개.
  • 근본 원인: "선언 크기" 를 가용·정책 상한과 무관하게 신뢰.

(B) 상한 없는 CopyTo — 압축 폭탄(ratio bomb) DoS (보안/리소스) ★간판

  • 분류 태그: decompression-bomb / unbounded-output.
  • 증상: inflate.CopyTo(outStream) 는 압축 스트림이 끝날 때까지 출력 제한 없이 MemoryStream 을 키운다. 고압축비 폭탄은 수 KB 입력이 수 GB 출력으로 팽창 → OOM + CPU. (A) 를 고쳐도 이쪽이 남으면 여전히 취약.
  • 재현조건: 고압축비 페이로드 1개.
  • 근본 원인: 해제 출력에 절대 상한·압축비 상한이 없다.

(C) produced/압축비 미검증 — 정합·정책 (버그/보안)

  • 증상: 풀어낸 크기가 선언과 다른지, 압축비가 정책을 넘는지 검사하지 않는다. 손상/변조 데이터를 통과시키고 폭탄 식별 신호도 버린다.
  • 근본 원인: 해제 결과 사후 검증/정책 부재.

(D) compressedLen 경계 미검사 — 예외/오버리드 (보안/안정성)

  • 증상: new MemoryStream(buf, 8, (int)compressedLen) 에서 8 + compressedLen > buf.LengthArgumentException(미처리 시 크래시). (int) 캐스팅은 큰 u32 를 음수로 만들어 또 다른 예외/논리오류. ReadU32 들도 buf.Length>=8 가정.
  • 근본 원인: 길이 필드를 실제 가용 바이트로 검증하지 않음.

수정안

핵심: ① 선할당 금지, ② 절대 출력 상한 + 압축비 상한, ③ 고정 청크로 읽으며 상한 초과 즉시 거부, ④ 경계·일치 검증.

public static class PacketCompression
{
    const int MaxOriginal = 8 * 1024 * 1024;   // 절대 출력 상한 (예: 8MB)
    const int MaxRatio    = 100;               // 압축비 상한 (출력/입력)

    public static byte[] DecompressPacket(byte[] buf)
    {
        if (buf.Length < 8) throw new InvalidDataException("short header");
        uint compressedLen = ReadU32(buf, 0);
        uint originalLen   = ReadU32(buf, 4);

        // (D) compressedLen 경계 검증 (int 오버플로/음수 방지)
        if (compressedLen > (uint)(buf.Length - 8))
            throw new InvalidDataException("compressedLen out of range");

        // (A)(C) 선언 크기는 상한 검사용일 뿐 — 선할당하지 않는다
        if (originalLen > MaxOriginal) throw new InvalidDataException("declared size too large");

        // 출력 상한 = min(절대 상한, 선언 크기, 입력 × 압축비 상한)
        long cap = Math.Min(MaxOriginal,
                   Math.Min((long)originalLen, (long)compressedLen * MaxRatio));

        using var input = new MemoryStream(buf, 8, (int)compressedLen, writable: false);
        using var inflate = new DeflateStream(input, CompressionMode.Decompress);

        // 출력 상한보다 1바이트 더 읽어보고, 초과하면 폭탄으로 간주해 거부
        var outBuf = new byte[cap + 1];
        int total = 0, read;
        while ((read = inflate.Read(outBuf, total, outBuf.Length - total)) > 0)
        {
            total += read;
            if (total > cap) throw new InvalidDataException("decompression bomb"); // (B) 즉시 중단
        }

        // (C) 선언과 실제 일치(정책)
        if (originalLen != 0 && total != originalLen)
            throw new InvalidDataException("size mismatch");

        var result = new byte[total];
        Array.Copy(outBuf, result, total);
        return result;
    }

    private static uint ReadU32(byte[] b, int off) =>
        (uint)(b[off] | (b[off + 1] << 8) | (b[off + 2] << 16) | (b[off + 3] << 24));
}

포인트

  • originalLen상한 검사용, 그 크기로 선할당하지 않는다(over-alloc 차단).
  • cap = min(절대상한, 선언크기, 입력×압축비상한) → 작은 입력은 작은 출력만 허용(ratio bomb 차단). cap+1 바이트까지 읽어 초과를 검출하면 즉시 거부(연결 차단/패킷 폐기).
  • compressedLen 경계와 int 캐스팅 안전성 확인, produced==originalLen 검증.
  • 더 큰 출력이 정당한 경로(맵 청크 등)는 그 타입의 상한을 따로 둔다.

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

  1. 메시지 종류별 상한 테이블: 타입마다 출력 상한·압축비를 선언하고 파서가 강제. 트레이드오프: 명세-코드 동기화.
  2. 스트리밍 + 누적 한도: 큰 정당 데이터는 청크 단위로 소비하며 누적 상한 enforce(한 번에 ToArray() 금지). 트레이드오프: 소비 측 스트리밍 처리 필요.
  3. 신뢰경계별 정책: 외부(클라) 입력은 보수적 상한, 내부(서버-서버 신뢰 채널)는 완화. 트레이드오프: 경로별 분기.
  4. 자원 쿼터/격리 + 퍼징: 해제에 메모리/시간 쿼터, 악성 입력 퍼징으로 강건성 검증.

면접 포인트 (예상 질문)

  1. new byte[originalLen] 가 왜 16바이트 패킷으로 OOM 을 일으키나? (int)compressedLen 캐스팅은 왜 위험한가?
  2. CopyTo 를 고정 청크 Read 루프 + cap+1 검출로 바꾸면 압축 폭탄을 어떻게 막는가? "절대 상한 + 압축비 상한" 을 함께 두는 이유는?
  3. 선언 originalLen 과 실제 produced 가 다를 때 거부해야 하는 이유, 그리고 정당한 대용량 데이터(맵 청크 등)와 폭탄을 어떻게 구분하겠는가?