10월 21일 Node.js 보안 릴리스에 실려 나온 CVE-2026-111497은 이름이 넷입니다. Node2Shell, Broccoli2Shell, Brotli2Shell, HTTP2Shell. 이름이 늘어난 이유는 하나입니다. 문제는 Node.js가 아니라 Node.js 안에 들어 있는 Google Brotli 디코더이고, 같은 디코더를 넣어 쓰는 Python 패키지, .NET, Bun, Electron, Envoy, curl 빌드도 영향을 받기 때문입니다. 미국 CISA는 10월 27일 이 취약점을 실제 악용 확인 목록(KEV)에 올렸고 연방기관 조치 기한은 11월 10일입니다.
이 글은 패치 버전표에서 끝나지 않습니다. 우리 서비스가 해당되는지 밖에서 먼저 보고, 서버 안에서 어떤 명령으로 확인하며, 무엇을 어떤 순서로 바꿔야 하는지까지 다룹니다. 코드에 brotli라는 단어가 한 줄도 없어도 영향을 받을 수 있습니다. Express 5가 요청 본문을 대신 풀고, 내장 fetch가 외부 서버 응답을 대신 풀기 때문입니다.
한눈에 보기
| 항목 | 내용 |
|---|---|
| 이름·CVE | HTTP2Shell(Node2Shell, Broccoli2Shell, Brotli2Shell로도 불림). CVE-2026-111497 |
| 어디가 문제인가 | Google Brotli 라이브러리의 디코더(압축 해제 부분). 잘못된 블록 전환 신호를 영구 오류가 아니라 '입력이 모자란 것'으로 보고해, 스트리밍 디코더가 8바이트 내부 버퍼에 입력을 계속 덧붙이며 메모리를 깨뜨린다(Node.js 공지 요약) |
| 영향 | Brotli 1.0.3 이상 1.2.0 이하(수정판 1.0.10·1.1.1·1.2.1 제외). 이 C 디코더를 포함하고 외부에서 받은 Brotli 데이터를 푸는 구성 |
| 결과 | 메모리 손상, 프로세스 크래시, 메모리 내용 노출, 원격 코드 실행 가능 |
| 점수 | NVD CVSS 3.1 9.8(치명), Google CNA CVSS 4.0 9.2(공격 전제 조건 있음), Microsoft와 Red Hat은 CVSS 3.1 8.1(높음)로 표기 |
| Node.js 수정 | 22.23.4, 24.22.1, 26.12.1(10월 21일). 이후 24.22.2·26.13.0(10월 28일). Node 20·23·25는 지원이 끝나 수정이 없다 |
| 악용 | 공개 PoC 10월 22일, 실제 공격 트래픽 10월 26일 관측(ELLIO), CISA KEV 등재 10월 27일(기한 11월 10일) |
| KISA 공지 | Node.js 제품 보안 업데이트 권고(10-23)에는 이 CVE가 없고, Google 제품 보안 업데이트 권고(10-26)에 'Google Brotli' 항목으로 실려 있다(보호나라 공지 목록 기준, 11월 3일) |
무엇이 문제인가: Node.js가 아니라 Brotli 디코더
Brotli는 구글이 만든 압축 방식입니다. 웹에서는 Content-Encoding: br로 응답을 줄여 보내는 데 쓰이고, 많은 서버 런타임과 HTTP 클라이언트 라이브러리가 구글의 C 디코더를 그대로 넣어 씁니다. 이번 결함은 그 디코더가 손상된 입력을 만났을 때의 처리에 있습니다. 공격자가 조작한 Brotli 스트림을 디코더에 먹이면 디코더가 내부 버퍼 경계를 넘겨 쓰고, 이 상태를 정교하게 맞추면 코드 실행까지 이어집니다. 영향은 '취약한 C 디코더를 포함하고, 외부에서 온 데이터를 그 디코더로 푸는 구성'에 한정됩니다. 브라우저는 제품별 공지를 따로 확인해야 하며 이 글에서는 다루지 않습니다.
수정은 세 갈래로 나왔습니다. Brotli 라이브러리 자체는 1.2.1(그리고 옛 계열용 수정판 1.1.1·1.0.10)에서 고쳤고, Node.js는 자기 안에 들어 있는 Brotli 복사본에 수정 커밋을 반영한 22.23.4·24.22.1·26.12.1을 냈습니다. 운영체제 배포판은 시스템 libbrotli 패키지나 nodejs 패키지를 고쳤습니다. 수정 커밋 자체는 올해 2월 27일 공개 저장소에 올라갔지만 보안 권고 없이 8개월을 지났습니다. 그래서 수정 릴리스가 나오기 전까지 취약한 릴리스 버전의 디코더를 그대로 포함한 소프트웨어에는 결함이 남아 있었습니다.
점수가 세 가지로 보이는 이유도 여기 있습니다. NVD의 CVSS 3.1은 9.8이지만 Google CNA의 CVSS 4.0은 '공격 전제 조건 있음'을 반영해 9.2이고, Microsoft와 Red Hat은 CVSS 3.1 기준 8.1로 매겼습니다. Red Hat은 플랫폼 보호 기능과 조작된 스트림이 필요하다는 점을 이유로 듭니다. 어느 체계든 '네트워크로, 인증 없이' 닿는다는 점은 같습니다.
공격은 어떻게 들어오나: 세 갈래
1. 들어오는 요청 본문
공격자가 Content-Encoding: br 헤더를 달고 조작한 본문을 보냅니다. 서버가 이 본문을 풀면 그 자리에서 디코더가 돕니다. Express 5(body-parser 2.x)는 기본 설정(inflate: true)에서 br 본문을 풉니다. Koa의 bodyparser, NestJS(Express 5 기반)도 같은 경로에 섭니다. 반면 Express 4(body-parser 1.x)는 br을 지원하지 않아 415(Unsupported Media Type)로 거부합니다. 국내에 많은 Express 4 레거시는 이 경로에서는 비교적 안전하지만, 아래 두 번째 경로는 그대로 남습니다.
2. 나가는 요청의 응답
Node.js 내장 fetch(undici)는 HTTPS 요청의 기본 Accept-Encoding에 br을 포함하고, 요청에서 br을 광고하지 않았더라도 응답이 br이면 자동으로 풉니다. axios의 Node 어댑터도 같습니다. 웹훅을 받은 뒤 상대 서버를 다시 부르는 콜백, 링크 미리보기, 크롤러, 사용자가 넣은 URL을 가져오는 기능이 모두 해당합니다. 공격자는 자기 서버가 br 응답을 돌려주게만 하면 됩니다. 이 경로는 요청 본문을 받지 않는 배치 서버와 백오피스에도 열려 있습니다.
3. zlib 직접 호출
zlib.brotliDecompress, createBrotliDecompress로 압축 파일이나 캐시, 메시지 큐 페이로드를 푸는 코드입니다. 입력이 외부에서 온 것이면 해당합니다.
세 갈래의 공통점은 '코드에 brotli가 없어도 프레임워크와 런타임이 대신 푼다'는 것입니다. 그래서 소스 검색만으로는 판정이 끝나지 않습니다.
우리 서비스가 해당되는지 확인하는 법
밖에서 보이는 신호로 먼저 추리고, 서버 안에서 확정합니다. 밖에서 보이는 것은 후보 신호일 뿐입니다. 응답 하나로 서버 전체가 안전하다거나 위험하다고 판정하지 않습니다.
| 구분 | 무엇을 보나 | 해석 |
|---|---|---|
| 밖 | Server·X-Powered-By 헤더의 Express 표기, 오류 화면에 드러나는 Node 버전 |
Node 서비스라는 흔적. 버전이 보이면 영향 버전인지 바로 비교. 흔적만 있고 버전이 없으면 '미확인' |
| 밖 | Content-Encoding: br 요청에 대한 응답 |
415면 br 본문을 풀지 않는 구성일 가능성(Express 4 계열, 또는 Express 5에서 inflate: false·문자셋 오류). 400 계열은 풀다가 실패했을 수도 있지만 인증·WAF·라우팅이 낸 4xx일 수도 있다. 파서 설정과 오류 유형, 요청 경로를 함께 봐야 한다 |
| 밖 | 수 초 간격의 503과 재접속 흔적 | 공격 시도로 워커(요청을 처리하는 프로세스)가 죽고 다시 뜨는 재시작 루프일 수 있다 |
| 안 | node --version |
공식 바이너리·nvm·Docker 공식 이미지라면 22.23.4, 24.22.1, 26.12.1 이상이 수정판. 20·23·25 계열은 수정이 없으니 업그레이드 대상 |
| 안 | 배포판 패키지(apt·dnf)나 관리형 런타임의 Node | 버전 번호가 아니라 공급자 보안 공지와 패키지 변경 이력으로 판정. 배포판마다 시스템 libbrotli를 쓰는 패키지와 번들 Brotli를 쓰는 패키지가 섞여 있으니 nodejs와 brotli 두 패키지의 공지를 모두 본다 |
| 안 | npm ls express body-parser @koa/bodyparser |
body-parser 2.x(Express 5)면 요청 본문 경로가 열려 있다 |
| 안 | 소스에서 brotliDecompress, createBrotliDecompress, fetch(, axios 검색 |
직접 호출과 나가는 요청 경로 확인 |
| 안 | pm2·systemd·컨테이너의 재시작 횟수 | 최근 급증했다면 크래시 공격을 받고 있을 수 있다 |
점검 함정
node -p process.versions.brotli값은 공식 빌드에서 패치 후에도 1.1.0 또는 1.2.0 그대로입니다. Node.js는 Brotli 버전을 올린 것이 아니라 수정 커밋만 반영했기 때문입니다(취약 태그와 수정 태그의 버전 파일이 같습니다). 공식 빌드는node --version으로, 배포판·관리형 런타임은 공급자 공지로 판정합니다. 이 값을 근거로 '수정됐다'고 쓴 외부 글을 그대로 옮기면 틀린 안내가 됩니다.
영향 범위 한 장
| 소프트웨어 | 영향과 수정 |
|---|---|
| Node.js | 22.23.4·24.22.1·26.12.1 미만 영향. 20·23·25는 지원 종료로 수정 없음 |
| Bun | 1.0.21 이상 1.4.4 미만 영향, 1.4.4 수정. bun build --compile 실행 파일은 재빌드 필요 |
| Deno | 비영향. 메모리 안전한 Rust 디코더를 쓴다(공식 PR에 명시) |
| Electron | 지원 중인 43·44·45가 Node 24 번들. 43.8.4·44.10.0·45.1.0(10-27)이 Node 24.22.1 반영. 앱은 올리고 재빌드·배포 |
| Python | Brotli 패키지 1.0.3 이상 1.2.1 미만, brotlicffi 1.0.3 이상 1.2.1.0 미만 영향. Brotli 또는 brotlicffi가 설치돼 있으면 requests(urllib3 기반)·httpx·aiohttp가 각자의 구현으로 br 응답을 자동 해제할 수 있다 |
| .NET | 8.0.33·9.0.22·10.0.14 미만 영향. BrotliStream, BrotliDecoder, HttpClient 자동 해제, ASP.NET Core 요청 해제 미들웨어(뒤의 둘은 기본 꺼짐). .NET Framework 비영향 |
| Java | brotli4j 1.24.0(10-24)이 첫 수정판. Netty 등이 사용 |
| Envoy·Istio | Brotli 압축 해제 필터를 켠 배포만 영향. Envoy 1.36.12·1.37.8·1.38.6·1.39.3·1.40.0, Istio 1.29.9·1.30.6·1.31.2 수정 |
| curl·git | brotli 지원으로 빌드된 libcurl이 Accept-Encoding 설정으로 응답을 풀 때 영향. git-remote-https도 같은 경로 |
| PHP(Windows 빌드) | 8.5.12-1·8.4.27-1·8.3.36-1·8.2.35-1(10-26)이 Brotli 1.2.1 포함 |
| OS 패키지 | Debian DSA-6582-1·DLA-4855-1, Ubuntu USN-8956-1/2, Red Hat·Amazon Linux 보안 공지. nodejs 패키지가 시스템 libbrotli를 쓰는지 번들을 쓰는지는 배포판·버전마다 달라 두 패키지의 공지를 모두 확인 |
| nginx ngx_brotli | 비영향. 압축(인코더)만 링크하고 디코더를 쓰지 않는다. 단, nginx는 br 요청 본문을 그대로 백엔드에 넘기므로 '프록시 뒤라서 안전'은 틀리다 |
대응 순서
오늘
- 외부에 열린 Node 서비스 목록을 실제 동작 기준으로 만든다. 도메인·서브도메인·관리자 콘솔·API 게이트웨이 뒤의 Node 앱까지.
- 서버마다 영향 여부를 가른다. 공식 빌드는
node --version, 배포판·관리형 런타임은 공급자 공지. 20 이하·23·25는 업그레이드 외에 방법이 없다. - 즉시 올릴 수 없는 서버는 임시 완화를 건다. 프록시·로드밸런서에서 신뢰하지 않는 클라이언트의
Content-Encoding: br요청을 거부하고, body-parser는inflate: false로 압축 본문을 받지 않게 한다. 나가는 요청은 주의가 필요하다. 내장 fetch에는 자동 해제를 끄는 옵션이 없으므로 패치 전에는 외부 URL을 가져오는 기능을 잠시 멈추거나, 자동으로 풀지 않는 HTTP 클라이언트로 br 응답을 거부하도록 바꾼다. axios는decompress: false로 자동 해제를 끌 수 있다. 어느 경우든 런타임 패치가 우선이다.
이번 주
- 런타임을 올리고 프로세스를 재시작한다. 실행 중인 프로세스는 재시작 전까지 옛 디코더를 쥐고 있다.
- 컨테이너는 수정된 베이스 이미지로 다시 빌드해 교체한다. 공식 이미지는 22.23.4·24.22.1·26.12.1 이상 태그를 쓴다. Node 20은 지원이 끝나 수정 태그가 없다.
- CI·빌드 환경의 Node와 락파일(package-lock.json)을 갱신한다. 빌드 머신도 fetch를 쓴다.
- Python·.NET·Java·Electron·Bun 등 다른 런타임의 Brotli를 같은 기준으로 점검한다.
이번 달
- 코어덤프(프로세스가 죽을 때 메모리를 통째로 저장한 파일) 저장 경로의 접근 권한을 좁히고 보관 기간을 줄인다. 실패한 공격이 남긴 덤프에는 세션 쿠키·토큰·키가 들어 있을 수 있다.
- 10월 17일 이후 로그를 점검한다(아래 신호 참고). 흔적이 있으면 노출됐던 서버의 비밀번호와 API 키를 바꾼다.
- 'Node 버전 노출'과 'br 요청 처리' 두 항목을 정기 외부 점검에 넣는다.
클라우드·PaaS별 체크
PaaS는 Vercel이나 Netlify처럼 서버를 직접 관리하지 않고 코드만 올리면 실행해 주는 서비스를 말합니다. 이런 환경은 공급자가 런타임을 고쳐도 내가 다시 배포해야 적용되는 경우가 많습니다.
| 환경 | 확인할 것 |
|---|---|
| Docker 공식 이미지 | 수정판 태그(22.23.4·24.22.2·26.13.0 등, alpine·bookworm·trixie·slim 변형 포함)가 올라와 있다(11월 3일 확인). 이미지만 바꾸고 재배포하지 않으면 그대로. node:20 태그는 지원 종료라 수정 태그가 없다 |
| Vercel | 10-21 WAF 규칙 배포, 10-28부터 새 배포가 수정된 Node 사용. 재배포해야 적용되고 Node 20 프로젝트는 22·24로 바꾼 뒤 재배포. WAF는 패치를 대체하지 않는다고 공식 공지에 명시 |
| Cloudflare | 10-25 관리형 WAF 규칙 2개가 추가됐지만 변경 로그 기준 기본 동작은 Log(차단 아님). 차단하려면 Block으로 바꿔야 한다 |
| AWS | 전용 보안 공지(2026-155-AWS, 10월 28일 게시·30일 갱신)가 있다. Lambda 관리형 런타임은 두 단계로 반영된다. 1단계는 함수를 새로 만들거나 갱신할 때, 2단계는 자동 런타임 갱신을 켠 함수에 자동 적용. 컨테이너 이미지 함수는 수정된 베이스 이미지로 재빌드·재배포해야 하고, 지원 종료 런타임(Node.js 18·20)은 보안 갱신이 없어 이전이 필요하다. Amazon Linux 2023은 brotli와 nodejs 패키지의 ALAS 공지가 따로 있다 |
| Netlify | 11월 3일 확인한 변경 로그에서는 관련 공지를 찾지 못했다. 빌드 이미지의 Node 버전을 직접 확인 |
악용 현황과 로그 신호
공개 PoC는 10월 22일 처음 올라왔습니다. ELLIO는 10월 26일 자사 센서에서 Node 24.22.0 x86-64를 노린 2,300만 바이트짜리 br 요청 본문을 관측했고, 그 전에는 node2shell-check/1.0 User-Agent로 br POST를 보내 디코딩 여부를 가려내는 정찰 트래픽이 있었다고 밝혔습니다.
공개된 연구 범위에서는 코드 실행에 빌드별로 맞춘 공격 코드가 필요했고, 맞지 않는 시도는 크래시로 끝났습니다. 그렇다고 '이론적 위험'은 아닙니다. 크래시는 쉽게 일으킬 수 있고, 프로세스 매니저가 매번 다시 띄워 주면 공격자는 계속 시도할 수 있습니다.
로그에서 볼 수 있는 신호의 예는 다음과 같습니다. 어느 것도 공격의 필수 증상은 아니며, 오류나 크래시가 없다고 안전한 것도, 오류 하나로 공격을 확정할 수 있는 것도 아닙니다.
- Node 취약판에서는
Z_BUF_ERROR가 늘 수 있다(정상적으로 잘린 입력에서도 나는 오류라 추세로 본다). 수정판은ERR__ERROR_FORMAT_BLOCK_SWITCH오류를 남긴다(막아 낸 흔적). - 워커 종료 코드 139(SIGSEGV)와 짧은 간격의 재시작.
- 연구자가 시험한 Python·PHP 스트리밍 디코더에서는 크래시 대신 출력 없이 멈추거나 CPU 사용률이 치솟는 증상이 관측됐다.
- 비정상적으로 큰 br POST 본문, 위의 정찰 User-Agent.
자가점검
1. 외부에 열린 Node 서비스 목록이 실제 동작 기준으로 있는가
도메인 목록이 아니라 '지금 응답하는 호스트' 기준이어야 합니다. 테스트 서버, 관리자 콘솔, 웹훅 수신기가 빠지기 쉽습니다.
2. 서버마다 판정 기준을 맞게 썼는가
공식 빌드는 node --version, 배포판·관리형 런타임은 공급자 공지. process.versions.brotli 로 판정하지 않았는지 다시 봅니다.
3. 요청 본문 경로를 확인했는가
body-parser 2.x(Express 5)·Koa·NestJS 사용 여부. Express 4라도 안심하지 말고 다음 항목으로 갑니다.
4. 나가는 요청 경로를 확인했는가
fetch·axios·undici 로 외부 URL 을 가져오는 기능(웹훅 콜백, 미리보기, 크롤러, 이미지 가져오기)이 있는지.
5. 배포판 패키지는 번호가 아니라 보안 공지로 판정했는가
nodejs 와 brotli 두 패키지의 변경 이력에 CVE-2026-111497 이 있는지 봅니다.
6. 올린 뒤 프로세스를 재시작하고 컨테이너를 다시 빌드했는가
설치만으로는 끝나지 않습니다. 실행 중인 프로세스와 옛 이미지가 그대로면 취약 디코더가 그대로입니다.
7. Node 20 이하 서버마다 업그레이드 일정이 정해져 있는가
수정이 나오지 않으므로 완화는 임시일 뿐입니다.
8. 10월 17일 이후 로그와 코어덤프를 점검하고 기록했는가
흔적이 있으면 비밀번호·키 교체까지 이어집니다.
KISA 공지는 어디에 있나
한국인터넷진흥원 보호나라 보안공지에서 이 CVE 를 찾으면 헷갈립니다. 10월 23일자 'Node.js 제품 보안 업데이트 권고'에는 같은 날 나온 다른 CVE 세 건만 있고 CVE-2026-111497 은 없습니다. 이 취약점은 10월 26일자 'Google 제품 보안 업데이트 권고'에 'Google Brotli' 항목으로 실려 있습니다(보호나라 공지 목록 기준, 11월 3일). 공지 제목만 보고 'Node 공지에 없으니 우리와 무관하다'고 판단하면 놓칩니다.
국내에는 온라인 진단 도구와 내부 점검 스크립트도 나와 있습니다. 어느 쪽을 쓰든 두 가지를 확인하십시오. 첫째, 외부에서 큰 br 본문을 던지는 방식은 서비스 중단을 일으킬 수 있으니 소유자 동의와 점검 창이 있을 때만 씁니다. 둘째, 버전 판정 기준이 무엇인지 봅니다.
밖에서 보이는 Node 서버부터 무료로 확인하기
SENTRIX 무료 취약점 점검의 HTTP2Shell 항목은 공격 패킷 없이 외부 응답의 헤더만 봅니다. 응답 헤더에 드러난 Node 버전 표기로 HTTP2Shell 영향 버전인지, 수정 버전 이상 표기인지, 지원이 끝난 계열인지를 가르고, Express 같은 프레임워크 흔적만 있고 버전이 없으면 '확인 필요'로 둡니다. 영향 버전 표기는 '풀어 주는 경로가 실제로 열려 있다'는 뜻이 아니라 서버 안 확인이 필요하다는 신호입니다. 같은 화면에서 PHP2Shell, 나모 크로스에디터, AI 서비스 노출도 함께 보고, 결과에는 서버 안에서 칠 확인 명령이 붙습니다. 대용량 br 프로브 같은 부하 검사는 무료 점검에 넣지 않고, 소유자 동의 아래 모의해킹 범위에서만 수행합니다.
주소 하나를 넣으면 보통 10~30초(대기 상황에 따라 더 걸릴 수 있음) 안에 밖에서 보이는 신호를 확인할 수 있습니다: 지금 무료 스캔. 결과에 Node 노출 흔적이 보이면 서버 안 확인과 조치 범위를 진단 문의하기에서 함께 정합니다. 점검 방식과 판정 읽는 법은 무료 취약점 점검: 주소만으로 PHP·Node.js·AI 서비스 노출 확인하는 법에, 같은 날 나온 PHP 쪽 대응은 PHP2Shell 대응 글에 정리해 두었습니다.
참고 자료
- NVD CVE-2026-111497
- Node.js 2026년 10월 보안 릴리스 공지
- Google Brotli 릴리스(v1.2.1·v1.1.1·v1.0.10)
- CISA Known Exploited Vulnerabilities 카탈로그
- ELLIO: Node2Shell(CVE-2026-111497) 분석과 야생 관측
- Bun 보안 권고 GHSA-663w-cv8g-jj2p
- Envoy 보안 권고 GHSA-3wvj-j6qm-62mr
- Python Brotli 권고 GHSA-9v8h-qqgm-4m35
- .NET 공지(dotnet/announcements #494)
- Vercel KB: Node.js security advisory
- Cloudflare WAF 관리형 규칙 변경 로그
- AWS 보안 공지 2026-155-AWS: Native Brotli Decoder RCE and Memory Disclosure
- Ubuntu 보안 추적기 CVE-2026-111497
- Red Hat CVE-2026-111497
- KISA 보호나라 보안공지: Google 제품 보안 업데이트 권고(2026-10-26)