36. 압축 해제(decompression bomb)의 크기 상한·비율 검증 부재 (C#)
난이도 상내 리뷰 · C#
내 리뷰 · C++
해설 · C#
해설 — 압축 해제(decompression bomb)의 크기 상한·비율 검증 부재 (C#)
난이도: 상
요약
신뢰할 수 없는 클라이언트가 보낸 압축 프레임을 해제하면서 공격자가 정한 declaredSize를 그대로 믿어 버퍼를 할당하고, **해제 출력에 상한(최대 크기·압축 비율)**을 두지 않는다. 공격자는 (1) declaredSize를 거대한 값(예: 2GB)으로 보내 즉시 대용량 할당을 유발하거나 음수/과대값으로 예외를 던지고, (2) 작은 압축 데이터가 거대하게 풀리는 **압축 폭탄(zip bomb)**으로 서버 메모리를 고갈시킨다.
문제점
1) [힌트 신뢰] declaredSize 로 버퍼 할당 (증상: 대용량 할당·예외·OOM)
- 증상: (A)에서 프레임 앞 4바이트를
declaredSize로 읽고 (B)에서new byte[declaredSize]. 이 값은 공격자가 정한다. 2GB 같은 값이면 즉시 거대한 할당(또는OutOfMemoryException/OverflowException), 음수면OverflowException. 실제 압축 내용과 무관하게 선언값만으로 자원을 소비한다. - 근본원인: 신뢰 경계를 넘은 크기 필드를 검증 없이 할당에 사용.
2) [출력 상한 부재] 압축 폭탄 (증상: 메모리 고갈 DoS)
- 증상: (C)의 루프는
output(=declaredSize 크기)까지 읽는다.declaredSize를 크게 잡고 그만큼 실제로 풀리는 데이터를 넣으면(작은 압축 → 큰 원본, 예: 반복 바이트) 서버가 그 전부를 메모리에 적재한다. 여러 연결이 동시에 하면 서버가 죽는다. 압축 비율(입력 대비 출력)에 상한이 없다. - 근본원인: "해제 출력 총량 ≤ 정책상 최대치" 및 "압축비 ≤ 상한" 검증 부재.
3) [프레임 경계·정합성 미검증] (부수)
- (A)는
frame.Length >= 4인지,declaredSize가 실제 해제 결과와 일치하는지 확인하지 않는다.declaredSize보다 실제 해제가 짧으면output뒤가 0으로 남고, 길면 (C)가 조기 종료해 데이터가 잘린다(선언과 실제 불일치를 검출 못 함).
수정안
정책상 최대 크기로 상한을 두고, declaredSize 는 상한 이내에서만 신뢰하며, 해제 중 누적 출력이 상한을 넘으면 중단.
public static byte[] Decompress(byte[] frame, int maxOutput) // maxOutput: 정책 상한
{
if (frame.Length < 4) throw new InvalidDataException("short frame");
int declared = BitConverter.ToInt32(frame, 0);
if (declared < 0 || declared > maxOutput) // 힌트 신뢰 상한
throw new InvalidDataException("declared size out of range");
using var input = new MemoryStream(frame, 4, frame.Length - 4);
using var deflate = new DeflateStream(input, CompressionMode.Decompress);
using var output = new MemoryStream(capacity: Math.Min(declared, 64 * 1024));
var buf = new byte[8192];
int total = 0, n;
while ((n = deflate.Read(buf, 0, buf.Length)) > 0)
{
total += n;
if (total > maxOutput) // 해제 중 누적 상한
throw new InvalidDataException("decompression bomb: output too large");
output.Write(buf, 0, n);
}
if (total != declared) // 선언 ↔ 실제 일치 검증
throw new InvalidDataException("declared/actual size mismatch");
return output.ToArray();
}
maxOutput(정책 상한)으로 선언값과 실제 출력 둘 다 제한한다. 버퍼는 선언값이 아니라 작은 청크로 스트리밍하며 누적하다가 상한 초과 시 즉시 중단.- 선언값과 실제 해제 크기 일치까지 검증해 프레임 위조를 걸러낸다.
더 나은 설계 (트레이드오프)
- 압축비 상한:
출력/입력비율 상한(예: 100:1)을 두면 폭탄을 조기에 차단. 트레이드오프: 정상 데이터의 최대 압축비를 넘지 않게 튜닝 필요. - 스트리밍 소비: 해제 결과를 통째로 메모리에 담지 말고 파서가 스트림으로 소비하면 피크 메모리를 줄인다. 트레이드오프: 파서가 스트리밍 지원해야.
- 프레임당·연결당 예산: 연결별 해제 총량/속도에 예산을 두고 초과 시 스로틀·킥. 트레이드오프: 회계 오버헤드.
면접 포인트
- "declaredSize 를 믿고 할당" 이 왜 위험한지(신뢰 경계)와, 해제 출력에 상한이 필요한 이유(압축 폭탄).
- 선언 크기·실제 해제 크기·압축비를 각각 검증하는 이유와 스트리밍 해제의 이점.
- 연결당 자원 예산·스로틀 같은 DoS 방어 설계.
해설 · C++
해설 — 압축 해제(decompression bomb)의 크기 상한·비율 검증 부재 (C++)
난이도: 상
요약
신뢰할 수 없는 클라이언트가 보낸 declaredSize를 그대로 믿어 버퍼를 할당하고, 해제 출력에 정책 상한·압축 비율 제한이 없다. C++ 에서는 여기에 더해 (i) frameLen < 4 검증이 없어 (A)의 memcpy(&declaredSize, frame, 4)가 OOB read 및 frameLen - 4 언더플로(거대한 size_t), (ii) Inflate 구현이 outCap을 무시하거나 넘겨 쓰면 힙 버퍼 오버플로가 될 수 있다. 공격자는 (1) 거대 declaredSize로 대용량 할당, (2) 압축 폭탄으로 OOM 을 유발한다.
문제점
1) [힌트 신뢰] declaredSize 로 버퍼 할당 (증상: 대용량 할당·bad_alloc·OOM)
- 증상: (A)에서 앞 4바이트를
declaredSize로 읽고 (B)에서std::vector<uint8_t> output(declaredSize). 공격자가 declaredSize 를 크게(예: 0xFFFFFFFF) 보내면 즉시 ~4GB 할당 시도 →std::bad_alloc/OOM. 실제 압축 내용과 무관하게 선언값만으로 자원 소비.
2) [출력 상한·압축비 부재] 압축 폭탄 (증상: 메모리 고갈 DoS)
- 증상: (C)는
declaredSize만큼 해제를 허용한다. 작은 압축 데이터가 거대하게 풀리도록 만든 폭탄이면 그 전부를 메모리에 적재. 압축비(입력 대비 출력)에 상한이 없어 여러 연결이 동시에 서버를 죽인다.
3) [경계·언더플로] frameLen 미검증 (증상: OOB read / 거대 size_t)
- 증상:
frameLen >= 4를 확인하지 않는다.frameLen이 0~3이면 (A)의memcpy(...,4)가 버퍼 밖을 읽고,frameLen - 4는size_t언더플로로 거대한 값이 되어Inflate에 잘못된 입력 길이를 넘긴다.
4) [출력 오버플로 위험] outCap 신뢰 (증상: 힙 오버플로)
- 증상:
Inflate에output.data()와declaredSize를 넘기지만, 실제 해제량이declaredSize를 초과할 때Inflate가outCap을 엄격히 지키지 않으면 힙 버퍼 오버플로. 선언 크기(할당)와 실제 해제량의 관계를 검증하지 않는다.
수정안
frameLen·declaredSize 검증, 정책 상한, 스트리밍 해제로 누적 상한 강제.
#include <stdexcept>
// zlib 스타일: 입력/출력을 청크로 스트리밍하며 상한을 지키는 인터페이스 가정
extern size_t InflateChunk(void* z, const uint8_t* in, size_t inLen,
uint8_t* out, size_t outCap, bool& done);
static std::vector<uint8_t> Decompress(const uint8_t* frame, size_t frameLen,
size_t maxOutput) {
if (frameLen < 4) throw std::runtime_error("short frame");
uint32_t declared;
std::memcpy(&declared, frame, 4);
if (declared > maxOutput) throw std::runtime_error("declared size out of range");
std::vector<uint8_t> output;
output.reserve(std::min<size_t>(declared, 64 * 1024)); // 힌트는 참고만
void* z = /* inflate 상태 초기화 */ nullptr;
uint8_t buf[8192];
const uint8_t* in = frame + 4;
size_t inLen = frameLen - 4;
bool done = false;
size_t total = 0;
while (!done) {
size_t n = InflateChunk(z, in, inLen, buf, sizeof(buf), done);
total += n;
if (total > maxOutput) // 해제 중 누적 상한
throw std::runtime_error("decompression bomb: output too large");
output.insert(output.end(), buf, buf + n);
if (n == 0 && !done) break; // 진전 없음 → 중단
}
if (total != declared) throw std::runtime_error("declared/actual mismatch");
return output;
}
- 스니펫 검증:
g++ -std=c++17 -fsyntax-only통과(자리표시자 선언 포함 시). - 버퍼를 declaredSize 로 통째 할당하지 않고 작은 청크로 스트리밍하며 누적이
maxOutput을 넘으면 즉시 중단. 출력 버퍼 상한을Inflate가 엄격히 지키게 청크 API 사용.
더 나은 설계 (트레이드오프)
- 압축비 상한:
출력/입력비율 상한(예: 100:1)으로 폭탄 조기 차단. 트레이드오프: 정상 최대 압축비 튜닝. - 스트리밍 소비: 해제 결과를 통째로 담지 말고 파서가 스트림으로 소비해 피크 메모리 축소.
- 연결당 예산: 연결별 해제 총량/속도 예산 + 초과 시 스로틀·킥.
면접 포인트
- declaredSize 신뢰·출력 상한 부재가 왜 위험한지(압축 폭탄), C++ 특유의 frameLen 언더플로·힙 오버플로 경로.
- 스트리밍 해제 + 누적 상한 + 압축비 제한의 방어 조합.
size_t부호 없는 산술의 언더플로 함정과 경계 검증 순서.