본인 소유이거나 점검 권한을 위임받은 서비스만 입력해야 합니다.
Node2Shell (CVE-2026-111497) 긴급 대응 방안
1. Node2Shell 요약
Node.js에 번들되거나 동적으로 링크된 Brotli 디코더의 취약점입니다. Node.js 측은 원격 코드 실행으로 이어질 수 있는 out-of-bounds write로 분류했습니다. 애플리케이션 코드와 관계없이 Node 버전만으로 영향을 받습니다. 다만 Node를 사용한다는 사실만으로 원격 공격이 성립하는 것은 아닙니다. 미패치 디코더와 공격자가 제어하는 Brotli 데이터가 디코더까지 도달하는 경로가 함께 존재해야 합니다. Express 5의 body-parser, Node의 전역 fetch() 등이 대표적인 진입점입니다.
항목 | 내용 |
CVE | |
심각도 | 높음(HIGH) - Node.js 공식 등급. “Brotli decoder out-of-bounds write permits remote code execution” |
수정 버전 | 22.23.4 / 24.22.1 / 26.12.1 (2026-10-21 배포) |
영향 | Node 프로세스 및 워커 크래시, CPU 고갈에 따른 서비스 거부, 조건부 메모리 노출 및 원격 코드 실행 |
영향 버전 | 수정 버전 미만의 22.x, 24.x, 26.x |
Node 20.x 이하 | 공식 릴리스 없음. 사용 중인 배포판(Ubuntu, Debian, RHEL 등)이나 LTS 지원 업체의 보안 업데이트를 확인해야 합니다. |
원인 | 디코더가 잘못된 block switch를 걸러내지 못하고 디코딩을 계속 수행합니다. upstream brotli v1.2.1에서 수정되었습니다. |
완화책 | 패치가 유일한 근본 조치이며, 적용 전까지는 Brotli 압축 해제 경로를 차단하는 임시 완화로 대응 시간을 확보할 수 있습니다. |
보고 / 수정 | 보고: OpenAI Codex Security(Khai Tran, Sylvie Mayer) |
동일한 보안 릴리스에 HIGH 등급이 3건 더 있습니다. dns.resolve의 NUL 문자 처리 오류(CVE-2026-105482), TLS 인증서 호스트명 우회 2건(CVE-2026-105483, CVE-2026-106069)입니다.
2. 취약한 환경 확인 방법
실제 서비스가 쓰는 디코더가 미패치 상태인지, 그리고 공격자가 디코더까지 도달할 수 있는 경로가 존재하는지 두 가지를 확인해야 합니다. 두 조건이 함께 존재하는 경우 실제로 원격 공격이 가능합니다.
2.1 런타임 패치 여부
Node.js 팀은 패치 과정에서 BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH 상수를 새로 추가했습니다. 해당 상수가 런타임에 존재하면 패치된 것으로 판단할 수 있습니다.
node -p "require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH ?? 'ABSENT'"
# -17 → 패치됨
# ABSENT → 미패치Node 버전 |
| 상수 마커 | 판정 |
22.23.3 | 1.1.0 | 없음 | 취약 |
22.23.4 | 1.1.0 | -17 | 패치 |
24.19.0 | 1.2.0 | 없음 | 취약 |
24.22.1 | 1.2.0 | -17 | 패치 |
26.12.0 | 1.2.0 | 없음 | 취약 |
26.12.1 | 1.2.0 | -17 | 패치 |
배포판 Node(--shared-brotli 빌드)에서는 상수만으로 디코더 패치 여부를 단정해서는 안 됩니다. 상수 값은 빌드 시점에 사용한 Brotli 헤더에서 결정되는 반면, 실제 디코더 코드는 런타임에 로드되는 libbrotlidec.so.1에 존재하기 때문입니다.
먼저 아래 명령으로 사용 중인 Node가 --shared-brotli 옵션으로 빌드됐는지 확인합니다.
node -p "process.config.variables.node_shared_brotli"node_shared_brotli가 true이면 현재 로드된 Brotli 버전과 배포판 패키지를 함께 확인합니다.
node -p "process.versions.brotli"shared-brotli 빌드는 다음 기준으로 조치합니다.
Brotli 버전 조건 | 판정 | 조치 |
1.0.x < 1.0.10 / 1.1.x < 1.1.1 / 1.2.x < 1.2.1 | 취약 | 수정된 패키지로 업데이트한 뒤 Node 프로세스 재시작 |
1.0.10+ / 1.1.1+ / 1.2.1+ | 패치 | Node 프로세스 재시작 후 재확인 |
process.versions.brotli는 인코더(libbrotlienc)의 버전이며, 해당 취약점은 디코더에 존재합니다. 동일한 소스 패키지에서 함께 배포되므로 일반적으로 버전이 일치하지만, 확실한 확인을 위해서는 디코더를 직접 점검하는 것이 좋습니다.
# Node를 실행하지 않고 라이브러리만 확인
strings /usr/lib/x86_64-linux-gnu/libbrotlidec.so.1 | grep -c ERROR_FORMAT_BLOCK_SWITCH
# 0이 아니면 패치됨번들 빌드의 node 바이너리도 동일한 방법으로 확인합니다. 컨테이너를 실행하지 않고 레지스트리 이미지만 점검할 때 활용할 수 있습니다.
2.2 노출 경로 확인
“우리는 Brotli를 사용하지 않는다”는 이유로 넘어가서는 안 됩니다. 코드에 Brotli가 존재하지 않는 경우에도 아래 경로에서 디코더가 동작합니다.
경로 | 판정 | 조건 |
Express 5 요청 본문 | O | body-parser 2.0.0부터 |
전역 | O | Node 18 이상이면 항상 해당됩니다. 응답의 Brotli 압축을 자동으로 풀기 때문에 의존성과 무관하게 노출됩니다. |
HTTP 클라이언트 | O | axios, got, node-fetch, superagent, undici, decompress-response 등이 응답 압축을 자동으로 풉니다. |
Express 4 요청 본문 | X | body-parser 1.x는 gzip과 deflate만 받고 |
Xint 팀이 직접 확인한 결과, Express 5에서는 express.json(), express.urlencoded(), express.text(), express.raw() 네 개 모두 Content-Type이 일치하는 요청이면 똑같이 Content-Encoding: br을 풉니다.
아웃바운드 경로도 공격 벡터가 될 수 있습니다. 웹훅 처리, 외부 API 연동, 피드나 이미지 가져오기 등 서버가 외부 URL에서 응답을 읽는 모든 경로가 해당합니다.
2.3 온라인 점검 서비스
별도 스크립트를 내려받거나 서버에서 실행하지 않아도 아래 페이지에서 Node2Shell 노출 가능성을 점검할 수 있습니다.
👉 Node.js Guard 바로가기: https://nodejsguard.io
점검할 서비스 URL을 입력합니다. (
https://를 포함한 전체 URL)점검이 끝나면 화면에 표시되는 판정과 권고 조치를 확인합니다.
운영 중인 도메인, 서브도메인, API 엔드포인트가 여러 개라면 각각 확인합니다.
패치 또는 설정 변경 후 동일한 URL을 다시 입력해 결과가 달라졌는지 확인합니다.
⚠️
웹 점검 결과는 외부에서 접근 가능한 경로를 기준으로 합니다. 사내망 전용 서비스, 외부에서 직접 접근할 수 없는 origin, batch/worker처럼 URL이 없는 Node 환경은 2.1의 방법으로 런타임을 직접 확인하고, 컨테이너 이미지와 배포판 보안 공지, 애플리케이션 구성을 별도로 점검해야 합니다.
3. 조치 방안
패치가 유일한 근본 조치입니다. 아래 임시 완화는 패치를 적용할 때까지 대응 시간을 확보하기 위한 수단입니다.
3.1 (P0) 패치 적용
22.x / 24.x / 26.x는 22.23.4 / 24.22.1 / 26.12.1 이상으로 업데이트합니다.
업데이트한 뒤, 프로세스를 반드시 재시작합니다. PM2나 systemd로 실행한 서비스는 바이너리를 교체해도 이전 프로세스가 그대로 유지됩니다.
컨테이너는 베이스 이미지를 다시 빌드해서 배포해야 합니다.
node:22-alpine같은 태그는 가리키는 이미지가 바뀌므로, 태그만 믿지 말고 새 이미지 안에서 버전을 직접 확인해야 합니다.배포 후 2.1의 방법으로 실제 서비스 Node를 다시 확인합니다.
Node 20 이하와 21/23/25 같은 지원 종료(EOL) 라인에는 upstream 공식 보안 릴리스가 없습니다. Node core의 Brotli 지원은 10.16.0과 11.7.0부터 들어갔으므로 그 이후 EOL 버전은 모두 영향 범위에 있습니다. 배포판이나 LTS 지원 업체가 보안 업데이트를 제공하는지 먼저 확인하고, 제공하지 않는다면 지원 버전으로 이전하는 것이 원칙입니다.
패치 우선순위는 노출 경로를 기준으로 정해야 합니다.
순위 | 대상 |
1 | 웹훅, 외부 API 연동, SSRF가 도달하는 경로로 외부 응답을 읽는 서비스 |
2 | Express 5 등으로 |
3 | 그 외 일반 일정 |
3.2 (P1) 패치 전까지 임시 완화
공격자가 Brotli 디코더에 도달하는 경로를 막는 방식입니다.
요청 본문 차단 — 리버스 프록시에서 거부하는 방법이 가장 명확합니다.
# nginx: br로 인코딩된 요청 본문을 거부
if ($http_content_encoding ~* "(^|,)\s*br\s*($|,)") { return 415; }애플리케이션에서 차단한다면 body-parser 앞단에 배치해야 합니다.
app.use((req, res, next) => {
const ce = String(req.headers['content-encoding'] || '').toLowerCase();
if (ce.split(',').some((e) => e.trim() === 'br')) return res.sendStatus(415);
next();
});⚠️
express.json({ inflate: false })로 inflate를 끄는 방법은 권장하지 않습니다. gzip과 deflate까지 함께 차단되어 정상 기능을 망가뜨릴 수 있습니다.
3.3 침해 여부 확인
해당 취약점은 압축 해제 단계에서 발생하기 때문에 애플리케이션 로그에는 “잘못된 요청”으로 남습니다. 공격이 실패해도 흔적은 남습니다.
① Brotli 디코딩 에러 급증
정상 트래픽에서는 거의 발생하지 않는 에러입니다. 급증하면 탐지 스캔이나 공격 시도를 의심할 수 있습니다.
grep -cE "ERR__ERROR_FORMAT_|Z_BUF_ERROR|BrotliDecoder|unexpected end of file" \
/var/log/app/*.log 2>/dev/null패치 전후로 에러 코드가 다릅니다. 로그에 어느 쪽이 기록되었는지 보면 당시 런타임 상태도 역으로 확인할 수 있습니다.
로그에 찍힌 값 | 당시 런타임 |
| 패치됨. 디코더가 정상적으로 차단했습니다. |
| 미패치. 잘못된 block switch를 지나친 다음 뒤늦게 오류가 발생했습니다. |
| 미패치일 수 있습니다. 요청이 중간에 끊겨도 발생하므로 동일한 IP에서 반복되는지 함께 확인합니다. |
② 워커 비정상 종료와 응답 지연
# 컨테이너 OOM과 재시작
docker ps -a --format '{{.Names}}\t{{.Status}}' | grep -iE "restart|exited"
kubectl get pods --field-selector=status.phase!=Running
# 커널이 남긴 흔적
dmesg | grep -iE "oom|segfault" | grep -i node원인을 알 수 없는 워커 재시작이 늘었거나, Content-Encoding: br 요청 직후에 응답이 멈추는 패턴이 관찰되면 함께 의심할 수 있습니다. 비동기 zlib은 libuv 스레드풀을 사용하기 때문에, 스레드 하나가 막히면 fs나 dns같은 무관한 기능도 함께 느려집니다.
4. 참고
Node.js 2026년 10월 보안 릴리스: https://nodejs.org/en/blog/vulnerability/october-2026-security-releases
Node.js v22.23.4 릴리스: https://nodejs.org/en/blog/release/v22.23.4
Node.js v24.22.1 릴리스: https://nodejs.org/en/blog/release/v24.22.1
Node.js v26.12.1 릴리스: https://nodejs.org/en/blog/release/v26.12.1
upstream brotli 수정 커밋: https://github.com/google/brotli/commit/0ed4268a4401abcb4472096340477cf4af7d1a2b
Node.js deps 체리픽 커밋: https://github.com/nodejs/node/commit/8c5debbea41dd50ed5b5bb9faf8ef442c5a794e4
Node.js 상수 노출 커밋: https://github.com/nodejs/node/commit/a18ec92752cd05ad5335bfaa5420a258b81f9afc
📩 최신 보안 인사이트 받아보기
Xint 팀이 분석한 취약점 연구와 최신 보안 인사이트를 이메일로 가장 먼저 받아보세요.
문의: contact@xint.io
웹사이트: xint.io