Node2Shell (CVE-2026-111497)긴급 대응 방안

인증 없이 서버 장악이 가능한 Node 취약점 Node2Shell(CVE-2026-111497). 직접 재현·검증한 취약 여부 확인 방법 및 조치방안을 확인하세요.
Xint's avatar
Oct 23, 2026
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

CVE-2026-111497

심각도

높음(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), 수정 : mcollina·aduh95

동일한 보안 릴리스에 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 버전

versions.brotli

상수 마커

판정

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"

shared-brotli가 true이면 현재 로드된 Brotli 버전과 배포판 패키지를 함께 확인합니다.

node -p "process.versions.brotli"

shared-Brotli는 다음 기준으로 조치합니다.

Brotli 버전 조건

판정

조치

< 1.0.10, < 1.1.1, < 1.2.1

취약

fixed package로 업데이트 후 Node 프로세스 재시작

>= 1.0.10, >= 1.1.1, >= 1.2.1

패치

Node 프로세스 재시작 후 재확인

process.versions.brotli는 인코더(libbrotlienc)의 버전이며, 해당 취약점은 디코더에 존재합니다. 동일한 소스 패키지에서 함께 배포되어 일반적으로 버전이 거의 어긋나지 않지만, 확실하게 확인하려면 디코더를 직접 점검하는 것이 좋습니다.

# 실행하지 않고 라이브러리만 확인합니다 (컨테이너 이미지 점검에도 그대로 사용합니다.)
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부터 Content-Encoding: br을 풉니다. inflate 기본값이 true이므로 별도로 활성화하는 설정이 없습니다.

전역 fetch()

O

Node 18 이상이면 항상 해당됩니다. 응답의 Brotli 압축을 자동으로 풀기 때문에 의존성과 무관하게 노출됩니다.

HTTP 클라이언트

O

axios, got, node-fetch, superagent, undici, decompress-response 등이 응답을 자동으로 해제합니다.

Express 4 요청 본문

X

body-parser 1.x는 gzip과 deflate만 받고 br은 415로 거부합니다.

Xint 팀이 직접 확인한 결과, Express 5에서는 express.json(), express.urlencoded(), express.text(), express.raw() 네 개 모두 똑같이 Content-Type 헤더에 따라서 Brotli 디코더를 호출합니다.
아웃바운드 경로도 공격 벡터가 될 수 있습니다. 웹훅 수신, 외부 API 연동, 피드나 이미지 가져오기 등 서버가 외부 URL에서 응답을 읽는 모든 경로가 해당합니다.

2.3 온라인 점검 서비스 (오픈 예정)

별도 스크립트를 내려받거나 서버에서 실행하지 않아도 아래 페이지에서 Node2Shell 노출 가능성을 점검할 수 있습니다.
https://nodejsguard.io

  1. 점검할 서비스 URL을 입력합니다. (https://를 포함한 전체 URL을 입력합니다.)

  2. 점검이 끝나면 화면에 표시되는 판정과 권고 조치를 확인합니다.

  3. 운영 중인 domain, subdomain, API endpoint가 여러 개라면 각각 확인합니다.

  4. 패치 또는 설정 변경 후 동일한 URL을 다시 입력해 결과가 달라졌는지 확인합니다.

⚠️ 본인 또는 점검 권한을 위임받은 서비스만 입력해야 합니다.

웹 점검 결과는 외부에서 접근 가능한 경로를 기준으로 합니다. 사내망 전용 서비스, 외부에서 직접 접근할 수 없는 origin, batch/worker처럼 URL이 없는 Node 환경은 node -v, container image, 배포판 advisory 및 애플리케이션 구성을 별도로 확인해야 합니다.

3. 조치 방안

패치가 유일한 근본 조치입니다. 아래 임시 완화는 패치를 적용할 때까지 대응 시간을 확보하기 위한 수단입니다.

3.1 (P0) 패치 적용

  1. 22.x / 24.x / 26.x는 22.23.4 / 24.22.1 / 26.12.1 이상으로 업데이트합니다.

  2. 업데이트한 뒤, 프로세스를 반드시 재시작합니다. PM2나 systemd로 실행한 서비스는 바이너리를 교체해도 이전 프로세스가 그대로 유지됩니다.

  3. 컨테이너는 베이스 이미지를 다시 빌드해서 배포해야 합니다. node:22-alpine 같은 이동 tag에 의존하지 않고 새 이미지 내부에서 버전을 확인해야 합니다.

  4. 배포 후 실제 서비스 Node로 다시 확인합니다.

Node 20 이하와 21/23/25 등 EOL line에는 upstream 공식 보안 릴리스가 없습니다. Node core Brotli는 10.16.0 및 11.7.0부터 제공됐습니다. 배포판이나 LTS 지원 업체가 보안 업데이트를 주는지 먼저 확인하고, 제공하지 않는다면 지원 버전으로 이전하는 것이 원칙입니다.

패치 우선순위는 노출 경로를 기준으로 정해야 합니다.

순위

대상

1

웹훅, 외부 API 연동, SSRF가 도달하는 경로로 외부 응답을 읽는 서비스

2

Express 5 등으로 Content-Encoding: br 요청 본문을 받는 서비스

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 })로 끄는 방법은 권장하지 않습니다. 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

패치 전후로 에러 코드가 다릅니다. 로그에 어느 쪽이 기록되었는지 보면 당시 런타임 상태도 역으로 확인할 수 있습니다.

로그에 찍힌 값

당시 런타임

ERR__ERROR_FORMAT_BLOCK_SWITCH (errno -17)

패치됨. 디코더가 정상적으로 차단했습니다.

ERR__ERROR_FORMAT_BLOCK_LENGTH_1 (errno -9)

미패치. 잘못된 block switch를 지나친 뒤 뒤늦게 오류가 발생했습니다.

Z_BUF_ERROR (errno -5)

미패치일 수 있습니다. 요청이 중간에 끊겨도 발생하므로 동일한 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. 참고

Share article