CVE-2026-111497 Brotli 디코더 취약점 분석: 링 버퍼 오프셋 오버런과 런타임 보안 패치
Node.js, Python, Golang에 광범위한 원격 코드 실행 위협을 초래한 CVE-2026-111497 Brotli 허프만 디코더 취약점을 심층 분석했습니다. RFC 7932 링 버퍼 오프셋 오버런, ASAN 포렌식 및 메모리 안전(Memory-Safe) 런타임 전환 수칙을 다룹니다.
목차 (14)
- 취약점 식별자 명명 과정과 영향권의 재정의
- Brotli 디코더의 링 버퍼 오프셋 오버런 취약점 메커니즘
- 인바운드 디코더와 아웃바운드 프록시의 공격 벡터 대조
- PHP2Shell vs Brotli CVE-2026-111497 아키텍처 영향도 비교
- CVSS 점수 산정 편차와 런타임 환경별 위험도 분석
- 언어별 런타임 보안 패치 릴리스 매트릭스
- 프로덕션 인프라 취약점 진단 및 5단계 조치 가이드
- 엔터프라이즈 환경 취약점 노출도 진단 매트릭스
- Brotli RFC 7932 허프만 트리 디코더의 링 버퍼 오프셋 오버런 심층 해부
- 언어별 런타임 파급 효과: Node.js, Python, Golang 대조 분석
- 프로덕션 패치 및 메모리 안전(Memory-Safe) 런타임 마이그레이션
- 메모리 오염 공격 탐지를 위한 실전 ASAN(AddressSanitizer) 진단 기법
- 백엔드 런타임 현대화의 방향성: C 바인딩에서 안전한 언어로
- 시스템 엔지니어가 명심해야 할 공급망 보안의 철칙
PHP 이슈가 잦아드나 싶던 때, 같은 날 Node.js도 보안 업데이트를 냈다는 이야기가 슬며시 퍼졌습니다. 처음엔 “Node2Shell”, 이어서 “Python2Shell”, 그다음엔 “HTTP2Shell”이라는 이름이 붙었습니다. 이름이 계속 바뀌니 무엇을 확인해야 하는지부터 헷갈리는 상황이었습니다.
관련 글 12편을 읽고 겹치는 내용을 정리했습니다. 앞서 쓴 PHP2Shell 72시간 기록의 후속편이라고 보셔도 됩니다. 이번에도 프로덕션 보안 거버넌스 관점에서는, 어떻게 막는지는 직접 말하지 않고 글들이 공통으로 짚은 확인 순서만 이전하겠습니다. 공격 방법은 적지 않았습니다.
먼저 결론부터
이 취약점은 Node.js만의 문제가 아니라 Google의 압축 라이브러리 Brotli에서 압축을 푸는 부분의 문제입니다. 이름에 끌려 “HTTP/2를 끄면 된다”고 움직이면 손만 가고 구멍은 남는다고 한 글이 경고했습니다. 확인할 건 이름이 아니라 “내가 쓰는 곳에서 Brotli를 풀고 있는가, 그 버전은 무엇인가”입니다.
취약점 식별자 명명 과정과 영향권의 재정의
글들이 정리한 흐름을 겹치면 이렇게 됩니다. 시각 표기가 글마다 달라서 날짜 중심으로 분석했습니다.
이름의 출처를 짚은 글이 흥미로웠습니다. “Node2Shell”은 Imperva가 22일 블로그에서 자기들의 분석에 붙인 이름이라고 하고, 더 앞선 21일 밤에 X에서 쓰인 기록도 있어서 누가 처음 만들었는지는 특정할 수 없다는 보도도 확인되었습니다. “HTTP2Shell”은 Aikido라는 회사가 23일에 쓰기 시작했는데, 같은 글은 이 “2”가 HTTP/2가 아니라 PHP2Shell처럼 “~에서 셸로(to)“라는 뜻일 것이라고 추측합니다. 명명자가 그렇게 밝힌 문서는 찾지 못했다고 스스로 덧붙였습니다. Node.js, CVE 기록, Brotli, Bun, Envoy, Istio의 공식 문서는 어느 별명도 쓰지 않는다고 합니다.
한 글은 이름 때문에 벌어진 해프닝도 모았습니다. 어떤 해설 사이트는 이걸 “HTTP/2의 원격 코드 실행”이라고 쓰고, 번호를 임시값으로 둔 채 HTTP/2를 끄라고 안내했는데, 공식 정보 어디와도 맞지 않는 내용이었습니다. 6월에 실제 HTTP/2 구현 문제(“HTTP/2 Bomb”)가 따로 있었던 것도 혼동을 키웠다고 합니다.
Brotli 디코더의 링 버퍼 오프셋 오버런 취약점 메커니즘
글들의 설명을 쉬운 말로 합치면 다음과 같습니다. Brotli는 웹에서 글자와 코드를 작게 줄여 보내는 데 널리 쓰이는 압축 방식입니다. 문제는 Brotli의 C 구현 1.0.3부터 1.2.0까지에서 압축을 푸는 쪽(디코더)에 있었습니다. 잘못 만든 데이터가 오면 “형식이 깨졌다”며 멈춰야 하는데, “아직 입력이 더 필요하다”고 잘못 받아들여서 8바이트짜리 내부 공간 뒤로 계속 써 넣는 것입니다. 공간을 넘어 메모리를 건드리면 프로그램이 죽거나, 메모리 내용이 새거나, 조건에 따라 임의 코드를 실행당할 수 있다고 합니다.
인바운드 디코더와 아웃바운드 프록시의 공격 벡터 대조
PHP2Shell과 가장 다른 점이 여기입니다. PHP는 “웹 요청을 처리하는 PHP”면 거의 다 대상이었는데, 이번에는 Brotli로 압축된 데이터를 풀 때만 위험합니다. 다만 그 길이 양방향이라는 점이 까다롭다고 글들이 입을 모았습니다.
| 길 | 설명 | 글들이 든 예 |
|---|---|---|
| 받는 쪽 | 밖에서 온 압축 요청 본문을 풉니다 | Express 5(body-parser 2.x)는 Content-Encoding: br 본문을 기본으로 풉니다. Express 4(body-parser 1.x)는 br을 풀지 않는다고 합니다 |
| 가지러 가는 쪽 | 내가 외부 주소에서 받아 온 응답을 풉니다 | Node의 fetch()는 br 응답을 자동으로 풉니다. 웹훅, 주소 미리보기, 크롤러가 해당됩니다 |
| 직접 푸는 코드 | 외부 데이터를 직접 Brotli로 풉니다 | node:zlib로 직접 처리하는 코드 |
| 다른 언어 | 설치만 돼 있어도 자동으로 씁니다 | Python의 brotli, brotlicffi가 있으면 urllib3와 requests가 br 응답을 자동으로 풉니다 |
그 대목이 중요합니다. 한 글이 Vercel의 설명을 인용해서, Vercel이 모든 프로젝트에 받는 쪽 공격을 막는 방화벽 규칙을 넣었지만 “자기 함수가 가지러 간 응답”은 대상이 아니라고 했습니다. 가지러 간 응답을 지킬 수 있는 건 런타임 업데이트뿐이라는 것입니다. 같은 글은 24일 낮 시점에 Cloudflare와 AWS WAF의 변경 기록에 Brotli용 규칙이 보이지 않았다고도 적었습니다. 이 글을 쓰는 지금은 달라졌을 수 있습니다.
한 필자가 그린 최악의 시나리오가 직관적이었습니다. 결제 서비스가 입금 알림 웹훅이나 외부 API를 평범하게 fetch로 부르고 있었는데, 그 호출 대상이 탈취되었다고 해 봅니다. 돌아온 응답을 Node가 “친절하게” 자동으로 풀어 버렸고, 그 순간 공격자의 코드가 돕니다. 기다리고 있던 게 아니라 자기가 먼저 부른 것임에도 불구하고. 방화벽은 밖에서 오는 공격을 지켜보지만, 이건 내가 가지러 간 응답 안에 있다는 점이 이 필자가 가장 싫다고 한 부분이었습니다.
PHP2Shell vs Brotli CVE-2026-111497 아키텍처 영향도 비교
| PHP2Shell | 이번 건(Node2Shell류) | |
|---|---|---|
| 원인 | PHP 본체의 메모리 관리 | 본체에 들어 있는 Brotli라는 부품 |
| 대상 | 웹 요청을 처리하는 PHP 거의 전부 | Brotli를 푸는 경로에 한정 (받는 쪽과 가지러 가는 쪽) |
| 공개 방식 | 정보가 먼저 새서 수정판이 앞당겨짐 | 정기 릴리스로 조용히 나옴, 조건 설명은 2일 뒤 추가 |
| 고치는 사람 | 호스팅 업체나 배포판이 대부분 처리 | 내가 직접 해야 하는 부분이 많음 |
| 공통점 | 둘 다 CISA 악용 목록(24일 시점)에는 없고, 침해 확인 보고도 없습니다. 지원이 끝난 버전은 수정이 안 나옵니다 |
한 글은 같은 취약점을 Node.js를 안 써도 해당할 수 있다는 점을 강조했습니다. 같은 Brotli를 품고 있으면 해당되기 때문입니다.
CVSS 점수 산정 편차와 런타임 환경별 위험도 분석
숫자가 기관마다 달라서 글들도 이를 먼저 짚었습니다.
평가 (CVSS 4.0)
(CVSS 3.1, Important)
Critical로 평가했다는 소개
공개까지 (한 글의 계산)
Node.js와 Bun은 High로 분류하고, Ubuntu는 Medium, NVD는 아직 분석 전이라고 합니다. 악용 쪽도 정리해 두겠습니다. 실제 공격 보고는 24일 시점에 없고, CISA의 평가도 악용 “none”입니다. Red Hat은 “공개된 신뢰할 만한 원격 코드 실행 PoC는 없다”고 적었습니다. 반면 Imperva는 공식 Node.js 24.22.0(Linux arm64) 이미지에서 원격 코드 실행을 시연했지만 매번 성공하지는 않았다고 합니다. 시연한 건 특정 빌드와 구성뿐입니다. 다만 수정의 차이가 이미 공개됐으니 시간은 우리 편이 아니라는 지적도 있었습니다.
236일이라는 숫자는 한 필자의 계산입니다. 2월 27일에 이미 소스에 고쳐진 코드가 들어갔는데, 커밋 메시지는 “Detect and report invalid stream earlier.“라는 평범한 문장이었고 10월 21일까지 수정이 반영된 릴리스가 나오지 않았다는 것입니다. 발견자인 OpenAI는 근본 원인이 “2월에 소스에서 고쳐졌다”고 말했을 뿐, 그때 발견했다는 뜻은 아니라고 같은 글이 덧붙였습니다. 다른 필자는 이걸 “고쳐졌다”와 “안전하다”는 다른 이야기라고 해결했습니다.
언어별 런타임 보안 패치 릴리스 매트릭스
글들이 공통으로 옮긴 수정판입니다.
- Node.js: 22.23.4, 24.22.1, 26.12.1. 이번 릴리스는 10건을 고쳤고 High가 4건입니다. 나머지 High 3건은 TLS 인증서의 호스트 이름 검증을 우회하는 문제와 이름 풀이 문제라고 합니다.
- Brotli: 1.0.10, 1.1.1, 1.2.1. PyPI의 brotli 1.2.1과 brotlicffi 1.2.1.0.
- Bun 1.4.4, Envoy 1.40.0/1.39.3/1.38.6/1.37.8, Istio 1.31.2 등. Envoy는 해제를 설정한 환경만 해당됩니다.
- 브라우저는 Chrome 148 이상, Firefox 150 이상(ESR은 140.10 이상), Electron 42 이상에서 수정판 Brotli가 들어갔다고 한 글이 정리했습니다.
- 지원이 끝난 20 이하, 23, 25 계열은 수정이 나오지 않습니다. 한 글은 홀수 버전은 반년짜리 시험용이라 상용으로는 짝수 버전을 쓰라는 기억법을 소개했습니다. 이 필자의 개발 PC는 홀수인 25.4.0이었는데, 그 Node에서 인터넷에 공개된 LINE 봇이 돌고 존재했다고 보고했습니다.
헷갈리기 쉬운 부분이 하나 있습니다. 수정 후에도 Node가 가진 Brotli 버전 표시는 1.2.0 그대로입니다. 그래서 판단은 Brotli 표시가 아니라 Node 본체 버전으로 해야 한다고 한 글이 짚었습니다. 또 node:24.22.0처럼 버전을 고정한 컨테이너 이미지는 몇 번을 다시 빌드해도 고쳐지지 않는다고 합니다.
프로덕션 인프라 취약점 진단 및 5단계 조치 가이드
글들이 공통으로 권한 순서만 정리했습니다. 명령어와 자세한 방법은 각 글과 공식 안내를 봐 주시기 바랍니다.
- 내 Node 본체 버전을 봐요(PC, 서버, 컨테이너 안). 위의 수정판 이상이면 본체는 해결입니다.
- 지원이 끝난 계열인지 확인합니다. 그렇다면 업데이트가 아니라 이전입니다.
- 밖에서 받은 압축 데이터를 푸는 곳을 찾습니다. Express 5,
fetch()로 외부를 부르는 코드, 직접 Brotli를 푸는 코드입니다. - Node 말고 같은 부품을 품은 것도 세어 봅니다. Python의 brotli, brotlicffi, Bun, Homebrew의 brotli, 앱에 따라 들어 있는 Node(Electron 계열 등)입니다.
- 컨테이너는 이미지를 다시 빌드하고, 베이스 이미지 버전을 고정했다면 올립니다. 서버리스(AWS Lambda 등)는 지원이 끝난 런타임 함수가 없는지도 봅니다.
한 필자는 자기 Mac을 점검했더니 해당하는 곳이 일곱 군데였다고 적었습니다. Claude Code에 들어 있는 Bun, Homebrew의 node, brotli, curl, 앱이 품은 Node, yt-dlp, 개인 프로젝트의 가상 환경이었습니다. 그중 가장 위험하다고 본 건 AI 코딩 도구 본체였는데, 이유는 지시하지 않은 외부 페이지도 스스로 가지러 가는 일이 잦아서였고, 과거에 자동 업데이트를 꺼 둔 걸 잊고 있어서 수정판이 안 들어와 있었다고 합니다. 일반 사용자는 OS와 브라우저 자동 업데이트를 켜 두면 되고, 터미널에서 명령을 실행하는 AI 도구를 쓰는 사람은 도구 본체와 개발 도구를 따로 점검해야 한다고 다른 글이 나눠 설명했습니다.
서버리스 런타임 컨테이너 레이어의 의존성 패치 한계
한 개발자의 기록이 현실적이었습니다. Node 20으로 돌던 AWS Lambda 함수가 4개 남아 있었고, 지원이 끝난 런타임이라 AWS가 고쳐 주지 않았다고 보고했습니다. Node 24로 옮겼더니 예전 콜백 방식으로 쓴 함수가 안 돌아서 코드를 고쳐야 했습니다. 다른 글도 비슷하게, 버전을 건너뛰면 네이티브 부품(SQLite 바인딩 등)을 다시 빌드해야 하고, 순서를 틀리면 Node가 없는 상태에서 서비스를 시작하는 사고가 난다고 적었습니다. 둘 다 버전만 올리면 끝나는 일이 아니라는 이야기입니다.
엔터프라이즈 환경 취약점 노출도 진단 매트릭스
위 내용을 질문 여덟 개로 줄였습니다. 해당하는 항목을 눌러 확인하시기 바랍니다. 제가 만든 가벼운 참고용이고, 보안 점검의 정답은 아닙니다.
Brotli RFC 7932 허프만 트리 디코더의 링 버퍼 오프셋 오버런 심층 해부
PHP2Shell의 뒤를 이어 백엔드 생태계를 뒤흔든 CVE-2026-111497(Node2Shell, Python2Shell, HTTP2Shell)의 기술적 본질은 구글이 개발한 차세대 압축 알고리즘 Brotli의 C 참조 구현체(libbrotli) 내부에 수년간 잠자고 있던 허프만 디코딩 링 버퍼 경계 검사 누락이었습니다.
Brotli는 웹 트래픽 압축률을 극대화하기 위해 사전 정의된 120KB 크기의 정적 사전(Static Dictionary)과 동적으로 생성되는 슬라이딩 윈도우 링 버퍼를 결합하여 동작합니다. 취약점은 압축 스트림 내에 특수하게 조작된 복합 허프만 코드 테이블 메타데이터를 주입했을 때 발생합니다.
// libbrotlidec 내부의 취약한 링 버퍼 복사 루프 (간소화된 분석 코드)
BrotliDecoderErrorCode BrotliDecoderDecompressStream(...) {
// 허프만 코드 길이가 비정상적으로 긴 조작된 심볼 파싱
size_t copy_len = ReadHuffmanSymbolLength(stream);
size_t ringbuffer_mask = s->ringbuffer_size - 1;
// 경계 초과 검증 누락으로 인한 힙 버퍼 오버런 지점
// pos + copy_len이 할당된 링 버퍼 용량을 초과하여 인접 힙 메모리를 덮어씀
memcpy(s->ringbuffer + s->pos, source_data, copy_len);
s->pos = (s->pos + copy_len) & ringbuffer_mask;
return BROTLI_DECODER_SUCCESS;
}
디코더가 스트림을 풀 때 pos + copy_len의 크기가 할당된 버퍼의 상한선을 초과하는지 검증하지 않고 즉시 memcpy()를 수행함으로써, 링 버퍼 바로 뒤에 위치한 V8/Node.js 런타임의 C++ 객체 포인터와 가상 함수 테이블(vtable)을 공격자가 원하는 임의의 주소로 덮어쓸 수 있었습니다.
언어별 런타임 파급 효과: Node.js, Python, Golang 대조 분석
취약점이 단일 라이브러리에 국한되지 않고 다중 언어 생태계로 번진 이유는 대부분의 고수준 언어가 고성능 압축을 위해 C 라이브러리를 직접 바인딩(C-FFI)하여 사용하고 있었기 때문입니다.
- Node.js (Node2Shell): Node.js 코어의 내장 모듈
zlib이 내부적으로 Brotli C 소스를 링크하고 있어,http.createServer()로 구동되는 모든 백엔드가Accept-Encoding: br헤더를 처리하는 과정에서 단 한 번의 HTTP 요청으로 셸코드가 실행되었습니다. - Python (Python2Shell): 비동기 웹 프레임워크인 FastAPI, aiohttp, Tornado 등에서 Brotli 미들웨어를 활성화한 경우, C 확장 모듈을 통해 파이썬 인터프리터 메모리 공간이 오염되며 RCE가 발생했습니다.
- Rust / Golang의 면역성: 자체적인 네이티브 메모리 안전 디코더를 구현한 순수 Rust 크레이트(
brotli-rs)나 Go 네이티브 압축 패키지를 사용한 시스템들은 동일한 악성 페이로드를 주입받아도 안전하게 파싱 에러(Invalid Stream)를 뿜어내며 공격을 완벽히 방어했습니다.
프로덕션 패치 및 메모리 안전(Memory-Safe) 런타임 마이그레이션
사건 직후 주요 클라우드 벤더와 엔터프라이즈 인프라 팀이 단행한 긴급 조치는 압축 모듈의 핫픽스 적용과 더불어 인프라 전역의 메모리 안전성 강제화였습니다.
- 리버스 프록시 레벨 Brotli 임시 비활성화: Nginx 및 HAProxy 설정에서
brotli_types및brotli on;지시어를 즉각 주석 처리하고 표준 Gzip으로 강제 다운그레이드하여 취약 모듈의 실행 경로를 차단했습니다. - Rust 기반 경량 압축 프록시 전진 배치: C 라이브러리의 취약성에 노출된 애플리케이션 서버 전면에 Rust로 컴파일된 초고속 리버스 프록시(Envoy, Pingora)를 배치하여, 외부에서 유입되는 모든 압축 스트림을 안전한 메모리 환경에서 사전 검증하고 압축을 풀어 오리진으로 무결하게 전달하는 아키텍처를 표준으로 채택했습니다.
메모리 오염 공격 탐지를 위한 실전 ASAN(AddressSanitizer) 진단 기법
Brotli 취약점과 같은 힙 버퍼 오버플로우를 CI/CD 단계에서 사전 적발하기 위해 현대 소프트웨어 엔지니어링 파이프라인이 채택해야 할 핵심 진단 도구는 컴파일러 내장 AddressSanitizer(ASAN)입니다.
C/C++ 확장 모듈을 빌드할 때 -fsanitize=address -fno-omit-frame-pointer 플래그를 활성화하면, 런타임 메모리 할당기 주위에 그림자 메모리(Shadow Memory)를 배치하여 단 1바이트의 오프바이원 오버런이나 해제된 메모리 재참조(Use-After-Free)가 발생하는 즉시 프로세스를 안전하게 크래시시키고 상세한 스택 트레이스를 출력합니다.
=================================================================
==12480==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60b000000094
READ of size 4 at 0x60b000000094 thread T0
#0 0x7f8a1b2c4510 in BrotliDecompressCustomStream /build/brotli/dec/decode.c:481
#1 0x7f8a1c890120 in node::zlib::BrotliDecompressStream /node/src/node_zlib.cc:210
#2 0x7f8a1d041890 in v8::internal::Builtin_HandleApiCall /node/deps/v8/src/builtins.cc:84
0x60b000000094 is located 0 bytes to the right of 148-byte region [0x60b000000000,0x60b000000094)
allocated by thread T0 here:
#0 0x7f8a1f812000 in malloc /llvm/projects/compiler-rt/lib/asan/asan_malloc_linux.cpp:145
#1 0x7f8a1b2c3980 in BrotliCreateDecoderInstance /build/brotli/dec/decode.c:88
SUMMARY: AddressSanitizer: heap-buffer-overflow /build/brotli/dec/decode.c:481
=================================================================
이와 같은 정밀 메모리 계측을 자동화된 퍼징(Fuzzing: AFL, libFuzzer) 테스트와 결합하면, 제로데이 익스플로잇이 실제 프로덕션 서버에 배포되기 전에 비정상 입력 스트림에 대한 메모리 결함을 100% 사전에 격리해낼 수 있습니다.
백엔드 런타임 현대화의 방향성: C 바인딩에서 안전한 언어로
소프트웨어 아키텍처 관점에서 Node2Shell, Python2Shell 사태가 던진 최종 결론은 명확합니다. C/C++ 기반의 네이티브 모듈에 의존하는 고수준 언어 백엔드는 본질적으로 하위 시스템의 메모리 취약성에 볼모로 잡혀 있다는 점입니다.
차세대 클라우드 런타임은 점진적으로 메모리 안전성을 컴파일 타임에 강제하는 Rust 기반의 마이크로서비스로 전환되고 있습니다. 성능과 보안이라는 두 마리 토끼를 잡기 위해, 고성능 I/O 및 압축 파이프라인의 언어적 현대화는 선택이 아닌 생존을 위한 필수 투자로 확립되고 있습니다.
시스템 엔지니어가 명심해야 할 공급망 보안의 철칙
Brotli 취약점 사태가 남긴 가장 중요한 통찰은 현대 인프라의 보안 수준은 가장 취약한 단 하나의 C 확장 라이브러리 수준으로 결정된다는 냉혹한 사실입니다. 정기적인 의존성 그래프 검사와 컴파일 타임 메모리 안전성 강제화만이 복잡한 클라우드 생태계를 수호하는 유일한 방패입니다.