pile·
백엔드·마켓컬리마켓컬리 Hello World·

엑셀 업로드 중 발생한 Zip Bomb 에러 파헤치기! 🥊

고객사가 올린 엑셀(.xlsx) 업로드가 Apache POI의 "Zip bomb detected" 오류로 막힌 사건을, 엑셀이 실은 압축 파일이라는 사실부터 POI의 압축비 검사 원리까지 풀어낸 글이다.

핵심 포인트
  • .xlsx는 여러 XML을 담은 ZIP 구조라, Apache POI가 XSSFWorkbook 생성 시 압축을 풀며 압축비(compressed/uncompressed)를 검사한다.
  • 압축 해제 크기와 원본 크기의 비율이 POI 기본값 MIN_INFLATE_RATIO(0.01, 약 100배)를 넘으면 Zip Bomb으로 보고 예외를 던진다.
  • 실제 원인은 악성 파일이 아니라 styles.xml에 동일 셀 스타일 정의가 과도하게 반복돼 압축률이 비정상적으로 높아진 정상 파일이었다.
  • 계약된 사용자만 접근하는 서비스라 공격 가능성이 낮다고 보고, 1차로 ZipSecureFile.setMinInflateRatio 값을 낮춰 검사를 완화했다.
상세 정리
  • 증상: 고객사 엑셀 업로드 기능에서 "Zip bomb detected! ... exceed the max ratio" 오류로 처리가 실패했다.
  • Zip Bomb 개념: 작게 압축돼 있지만 풀면 메모리·디스크를 폭증시켜 시스템을 마비시키는 공격 방식으로, 열기 전엔 예측이 어렵다.
  • 엑셀=압축파일: unzip으로 풀면 xl, docProps, _rels 등 폴더와 XML이 나오고, POI는 이 ZIP을 풀며 각 항목의 압축비를 본다.
  • 로그 해석: Uncompressed 406,416 bytes, compressed 4,059 bytes, Ratio 0.009987로, 시스템 한계 0.010000을 근소하게 넘겨 경고가 발생했다.
  • 범인 파일: 문제 엔트리는 xl/styles.xml로, dxf(폰트·fill 등) 셀 스타일 정의가 반복돼 있었다.
  • 왜 압축률이 튀나: ZIP/gzip은 반복 패턴을 짧은 코드로 치환하므로 같은 스타일 반복이 많을수록 압축률이 극단적으로 높아져 Zip Bomb으로 오인된다.
  • 방어 방법 검토: (1) setMinInflateRatio로 최소 압축비 조정, (2) 각 엔트리 getSize 합으로 총 해제 크기 제한(MAX_ALLOWED_SIZE), (3) getDepth로 중첩 압축 레벨 제한.
  • 최종 조치: 신뢰된 사용자 대상이라 판단해 압축비 검사 완화를 1차 적용하고, 근본적 보안 대책은 추후 마련하기로 했다.
왜 읽나Apache POI로 엑셀 업로드를 처리하다 Zip bomb 오탐을 만난 백엔드 개발자에게 원인(스타일 반복·압축비)과 대응 옵션을 알려주는 사례.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2