Node.js 취약점: Node2Shell 패치와 Node.js 버전 확인 (워드프레스)

공정위 광고 문구 Ver 3.1
공정위 광고 문구 Ver 3.2(모바일 전용)
테스트 환경 RHEL 계열AlmaLinux 9.8Nginx + Apache · WordPress 7.1.32026-10-23 확인

Node2Shell은 2026년 10월 21일 패치가 공개된 Node.js 취약점 CVE-2026-111497의 별칭으로, 공식 제목은 구글 Brotli 디코더의 경계 밖 쓰기 결함입니다. 조작된 Brotli 압축 데이터를 푸는 순간 원격 코드 실행으로 이어질 수 있어, 서버의 Node.js·Bun·libbrotli 버전을 확인해 패치 버전으로 올리는 일이 먼저입니다.

워드프레스 7.1.3이 올라간 Nginx + Apache 구성 서버에서 Brotli 디코더를 쓰는 패키지와, 워드프레스 원격 요청이 libbrotli를 거치는 경로를 확인한 과정입니다.

1️⃣ Node.js 취약점 Node2Shell: 결함 원리와 영향 범위

Node2Shell이라는 이름은 보안 업체 Imperva의 연구팀이 10월 22일 연구 글에서 붙인 별칭입니다. nodejs.org 공지의 제목은 [Brotli decoder out-of-bounds write permits remote code execution]입니다. 결함은 Node.js가 내장한 구글 Brotli 디코더 소스(c/dec/decode.c)에 있어, 애플리케이션 코드를 고치는 것으로는 막히지 않습니다.

NVD 기록 기준으로 구글 Brotli 1.0.3~1.2.0 디코더는 조작된 압축 스트림을 풀 때 버퍼 끝을 넘어 기록하며, 인접한 디코더 상태 메모리를 오염시킵니다. 로그인 없는 원격 공격자가 이 결함으로 서버 메모리 내용을 읽어 내거나 서비스를 중단시키고, 임의 코드를 실행할 수 있습니다.

위험도 표기는 둘입니다. nodejs.org 공지는 High 등급을 매겼고, CVE를 낸 구글은 CVSS 4.0 기준 9.2점(Critical)을 매겼습니다. NVD는 10월 23일 기준 자체 분석 전이라 NVD 점수는 아직 없습니다. 약점 유형은 반환값 미검증(CWE-252)과 경계 밖 쓰기(CWE-787)입니다.

nodejs.org 보안 공지 기준으로 Node.js 22.x·24.x·26.x 지원 계열이 모두 영향권에 들어가며, 패치 버전은 22.23.4·24.22.1·26.12.1입니다. 지원이 끝난 계열은 보안 공지가 나올 때마다 늘 영향권에 든다고 같은 공지가 밝히고 있습니다.

같은 디코더를 쓰는 프로그램은 Node.js 밖에서도 모두 대상입니다. 구글은 10월 21일 같은 CVE를 고친 Brotli 라이브러리 1.0.10·1.1.1·1.2.1 세 버전을 냈습니다. Bun은 10월 22일 1.4.4 공지의 보안 수정 목록에 Brotli를 올렸고, 저장소 이슈는 그 버전이 brotli를 갱신한 고침을 담았다고 밝히고 있습니다.

대상영향 범위패치 버전근거
Node.js22.x · 24.x · 26.x 지원 계열 전부 · 지원이 끝난 계열22.23.4 · 24.22.1 · 26.12.1nodejs.org 보안 공지(2026-10-21)
구글 Brotli 라이브러리1.0.3 ~ 1.2.01.0.10 · 1.1.1 · 1.2.1NVD 기록 · google/brotli 릴리스(2026-10-21)
Bun1.4.4 공지의 보안 수정 목록에 Brotli1.4.4Bun 공지(2026-10-22)
배포판 libbrotli 패키지Brotli 1.0.3 ~ 1.2.0 을 바탕으로 만든 패키지배포판 보안 공지가 내는 버전(2026-10-23 기준 아직 없음)각 배포판 보안 추적 페이지

Node.js 자체는 받은 Brotli 데이터를 스스로 풀지 않습니다. 호스팅 업체 Vercel의 보안 공지 기준으로, 노출되는 자리는 앱이나 라이브러리가 바깥에서 온 압축 데이터를 푸는 곳입니다. 내장 fetch()가 br 응답을 받을 때와 Express 5·body-parser 2가 br 요청 본문을 풀 때가 그 예이며, 받기만 하는 기본 HTTP 서버는 풀지 않습니다.

실제 악용은 10월 23일 기준 확인되지 않았습니다. CISA의 알려진 악용 취약점 목록(KEV, 10월 21일 버전)에는 이 CVE가 없습니다. CVE 기록에 CISA가 매기는 악용 단계는 10월 23일 [개념 증명(PoC) 공개]로 바뀌었습니다.

디코더가 어디에 들어 있느냐가 할 일을 정합니다. Node.js 공식 바이너리는 디코더를 실행 파일 안에 내장하고, Debian의 nodejs 패키지처럼 시스템 libbrotli를 쓰는 빌드도 있습니다. 리눅스 배포판은 같은 소스를 libbrotli 패키지로 설치해 curl 같은 프로그램이 공용으로 사용합니다.

2️⃣ 워드프레스·PHP 사이트도 해당: 원격 요청이 libbrotli로 응답을 푸는 경로

Node.js를 설치하지 않은 워드프레스 서버에도 같은 디코더가 libbrotli 패키지로 들어 있습니다. 워드프레스·curl·PHP 쪽에서 이 CVE를 다룬 공식 언급은 10월 23일 기준 없고, curl 공식 취약점 목록에도 이 CVE는 없습니다. 그래서 이 장은 코어 코드와 curl 문서, 테스트 서버 실측으로 구조만 확인한 것입니다.

실측한 Nginx + Apache 구성 서버에서 디코더 라이브러리(libbrotlidec)를 요구하는 패키지는 다섯이었고, 그중 워드프레스와 이어지는 것은 libcurl입니다.

rpm -q --whatrequires 'libbrotlidec.so.1()(64bit)' | sed 's/-[0-9].*//'
freetype
brotli-devel
libjxl
libcurl
libsoup

libcurl이 libbrotli를 요구하는 까닭은 압축된 HTTP 응답을 푸는 데 있습니다. curl 공식 문서 기준으로 brotli 라이브러리와 함께 빌드된 libcurl은 Content-Encoding이 br인 응답을 자동으로 풉니다. 그 libcurl을 PHP의 curl 확장이 그대로 호출합니다.

ldd /usr/lib64/libcurl.so.4 | grep brotli
    libbrotlidec.so.1 => /lib64/libbrotlidec.so.1 (0x00007f32803ed000)
    libbrotlicommon.so.1 => /lib64/libbrotlicommon.so.1 (0x00007f327f858000)
php -i | grep BROTLI
BROTLI => Yes

워드프레스가 쓰는 libcurl 버전은 관리자 화면 [도구 › 사이트 건강]의 [정보] 탭에서 [서버] 구역을 펼치면 [cURL 버전] 줄에 표시됩니다. brotli 지원 여부는 그 줄에 나오지 않으므로 위처럼 서버 명령으로 확인합니다.

사이트 건강 정보 탭 화면: 왼쪽 메뉴 도구 › 사이트 건강, 서버 구역의 서버 구조·웹 서버·PHP 버전·PHP SAPI·cURL 버전 줄 — Node.js 취약점 Node2Shell의 워드프레스 쪽 경로인 libcurl 버전을 cURL 버전 줄에서 확인(워드프레스 7.1.3)

그림의 [cURL 버전] 줄은 libcurl 버전과 함께 빌드된 SSL 라이브러리를 표시합니다. curl 문서 기준으로 br은 7.57.0부터 지원되는 방식이라, 이 값이 그보다 낮으면 br 응답을 풀지 못합니다. 그 이상이면 빌드에 brotli가 들어갔는지를 서버 명령으로 봅니다.

워드프레스 코어의 Requests 라이브러리는 바깥 주소에 요청할 때 curl 핸들의 [CURLOPT_ENCODING] 값을 빈 문자열로 설정합니다. curl 문서 기준으로 빈 문자열은 그 libcurl이 지원하는 모든 압축 방식을 받겠다는 뜻입니다. 그래서 상대 서버가 br로 압축한 응답을 보내면 워드프레스 서버의 libbrotli가 그것을 풀게 됩니다.

grep -n CURLOPT_ENCODING wp-includes/Requests/src/Transport/Curl.php
110:            curl_setopt($this->handle, CURLOPT_ENCODING, '');
212:            curl_setopt($this->handle, CURLOPT_ENCODING, 'none');

110행이 모든 요청의 기본값이고, 212행은 압축을 푸는 데 실패한 요청을 압축 없이 다시 보낼 때만 실행됩니다. 밖에서 직접 찌르는 길은 없고, 코어·플러그인 업데이트 확인이나 oEmbed, RSS 피드처럼 워드프레스가 응답을 받아 오는 상대 서버가 먼저 장악돼야 합니다.

🧩 Node2Shell 공격 흐름
설명 다이어그램
①바깥에서 오는 압축 데이터br 로 압축된 요청 본문이나 상대 서버의 응답
②앱이 Brotli 디코더로 푸는 단계Node.js fetch() · Express 5 · Bun fetch() · 워드프레스는 libcurl → libbrotli
③Brotli 디코더 결함조작된 스트림이 버퍼 끝을 넘어 기록CVE-2026-111497
④서버에서 코드 실행메모리 유출 · 서비스 중단 · 서버 장악
✅ 패치 버전에서는 이 흐름이 막힘Node.js 22.23.4 · 24.22.1 · 26.12.1Bun 1.4.4Brotli 1.0.10 · 1.1.1 · 1.2.1

흐름은 바깥에서 온 br 압축 데이터로 시작합니다. Node.js의 fetch()나 Express 5, Bun의 fetch(), 워드프레스의 libcurl이 호출한 libbrotli가 그것을 푸는 단계에서 결함을 건드립니다. 그 결과가 서버에서 코드 실행으로 이어집니다. 패치 버전은 이 흐름을 막으므로 할 일은 디코더를 패치 버전으로 바꾸는 것입니다.

Apache의 mod_brotli는 응답을 압축하는 인코더 라이브러리(libbrotlienc)만 링크하므로, 이 모듈로 압축을 켜 둔 설정은 디코더 결함의 경로에 들지 않습니다.

ldd /usr/lib64/httpd/modules/mod_brotli.so | grep brotli
    libbrotlienc.so.1 => /lib64/libbrotlienc.so.1 (0x00007f34b4b79000)
    libbrotlicommon.so.1 => /lib64/libbrotlicommon.so.1 (0x00007f34b4a79000)

3️⃣ Node.js 버전 확인: 서버에 Node.js·Bun·libbrotli가 있는지와 버전

할 일은 서버에 무엇이 설치돼 있는지 세는 것부터입니다. Node.js는 [node -v]로 버전을 보고, 그 값이 22.23.4·24.22.1·26.12.1보다 낮으면 영향권입니다.

내 환경확인 명령고쳐지는 곳
nodejs.org 설치 파일 · nvm · Docker 공식 이미지[node -v]Node.js 자체(22.23.4 · 24.22.1 · 26.12.1)
배포판 nodejs · ldd에 libbrotli 줄 있음[ldd $(command -v node)]libbrotli 패키지
배포판 nodejs · ldd 결과 0[ldd $(command -v node)]nodejs 패키지(배포판 보안 공지 뒤)
공유 호스팅 Node.js Selector(CloudLinux alt-nodejs)업체 공지 · 패키지 release 번호업체가 배포한 패키지 + 앱 다시 시작
Bun[bun --version]Bun 1.4.4 이상
Node.js 없음(워드프레스만)[ldd]로 libcurl 확인libbrotli 패키지 + php-fpm 재시작 · 그 전엔 임시 조치
관리형 호스팅업체 공지 · 문의업체가 올리는 패키지

PC(Windows·macOS)는 nodejs.org의 패치 버전 설치 파일로, nvm은 [nvm ls]로 버전을 본 뒤 [nvm install 24]처럼 계열 번호로 다시 설치합니다. Docker 공식 node 이미지의 22·24·26 태그는 10월 22일 패치 버전으로 다시 만들어졌으므로, 이미지를 다시 받아 빌드하고 배포합니다. Vercel 공지는 직접 빌드한 컨테이너 이미지와 CI 실행 환경, 로컬 개발 환경도 패치 버전으로 올리라고 권합니다.

dnf module 스트림으로 Node.js를 설치한 서버는 [node -v]에 그 스트림 계열의 버전이 나오며, 계열은 같아도 끝자리가 패치 버전에 못 미치면 올려야 합니다.

node -v
v24.xx.x

Node.js가 시스템의 libbrotli를 쓰는지 내장 디코더를 쓰는지는 [ldd]로 확인합니다. 출력에 libbrotlidec 줄이 있으면 공용 라이브러리를 쓰는 빌드이고, 아래처럼 0이면 내장 디코더라 Node.js 자체를 올려야 고쳐집니다.

ldd $(command -v node) | grep -c brotli
0

Bun은 [bun --version]으로 버전을 확인하고 1.4.4보다 낮으면 올립니다.

libbrotli 패키지는 RPM 계열에서 [rpm -q libbrotli]로 버전을 봅니다. 패키지 버전 번호는 배포판이 붙인 것이라 구글 릴리스 번호와 바로 비교하지 않고, 배포판 보안 공지가 이 CVE 번호로 낸 버전과 비교합니다.

rpm -q libbrotli
libbrotli-1.0.x-x.el9_x.x86_64

배포판 공지가 나왔는지는 [dnf updateinfo info --cve CVE-2026-111497]로 확인합니다. 출력이 비어 있으면 그 저장소에 아직 이 CVE용 공지가 없는 것입니다.

dnf updateinfo info --cve CVE-2026-111497

버전 번호를 그대로 둔 채 고침만 옮겨 넣은 패키지는 변경 기록에 CVE 번호가 남습니다. 그래서 [rpm -q --changelog libbrotli | grep CVE-2026-111497]로 확인하며, 줄이 나오면 패치된 패키지입니다.

rpm -q --changelog libbrotli | grep CVE-2026-111497

10월 23일 기준으로 Debian·Ubuntu·Red Hat·AlmaLinux의 libbrotli 패키지에는 이 CVE의 패치 버전이 아직 없습니다. Alpine은 nodejs 패키지만 패치 버전이 나왔고, 근거는 각 배포판의 보안 추적 페이지입니다. 그래서 지금은 배포판 보안 공지가 나오는지를 지켜보다가 나오면 올리는 순서가 됩니다.

Debian·Ubuntu 계열은 패키지 이름이 libbrotli1이며, 한 관리형 호스팅의 Debian 12 테스트 서버에서 같은 항목을 확인했습니다. 이 서버에서도 libcurl이 libbrotlidec을 링크하고 PHP의 curl 확장이 brotli를 지원했습니다. 변경 기록은 [zcat /usr/share/doc/libbrotli1/changelog.Debian.gz | grep CVE-2026-111497]로 봅니다.

dpkg -l libbrotli1 | tail -1
ii  libbrotli1:amd64 1.1.0-2+0~20240305.8+debian12~1.gbp62df71 amd64        library implementing brotli encoder and decoder (shared libraries)
ldd /usr/lib/x86_64-linux-gnu/libcurl.so.4 | grep brotli
    libbrotlidec.so.1 => /lib/x86_64-linux-gnu/libbrotlidec.so.1 (0x00007fa2baac6000)
    libbrotlicommon.so.1 => /lib/x86_64-linux-gnu/libbrotlicommon.so.1 (0x00007fa2b9cb9000)

4️⃣ Node.js 업데이트: 패치 버전으로 올리고 다시 확인

관리형·공유 호스팅은 서버 패키지를 업체가 올리므로 Node.js와 libbrotli의 패치 적용 여부를 업체 공지나 문의로 확인합니다. 서버를 직접 운영한다면 Node.js는 nodejs.org 공지의 패치 버전으로, Bun은 [bun upgrade]로 1.4.4 이상으로 올립니다.

배포판 모듈 스트림으로 설치한 Node.js는 그 저장소에 패치 버전이 올라온 뒤 [dnf upgrade nodejs] 꼴로 올립니다. nodejs.org에서 직접 내려받아 설치한 서버는 같은 자리의 패치 버전으로 바꿉니다. 지원이 끝난 계열은 패치 버전이 나오지 않으므로 지원 중인 22·24·26 계열로 옮깁니다.

공유 호스팅의 Node.js Selector(CloudLinux alt-nodejs)는 brotli 사본을 따로 품어 시스템 libbrotli를 올려도 고쳐지지 않습니다. CloudLinux 공지 기준으로 업체가 배포한 패키지로 바뀐 뒤 앱을 다시 띄워야 새 바이너리가 실행됩니다. 지원이 끝난 버전은 [node --version] 값이 그대로라 패키지 release 번호로 확인한다고 같은 공지가 밝혔습니다.

libbrotli 패키지는 배포판 보안 공지가 나오면 그 패키지를 올리고, 라이브러리를 메모리에 올려 둔 프로세스를 다시 시작해야 새 라이브러리로 바뀝니다. 공유 라이브러리는 프로세스가 시작할 때 읽히므로, 워드프레스 서버에서는 php-fpm 재시작까지가 한 묶음입니다.

올린 뒤에는 [node -v]가 패치 버전 이상인지와 [rpm -q libbrotli]가 배포판 공지의 버전인지를 봅니다. 변경 기록에 CVE 번호가 들어갔는지는 [rpm -q --changelog libbrotli | grep CVE-2026-111497]로 확인합니다. php-fpm을 다시 시작했으면 서비스 상태와 사이트 응답까지 확인합니다.

systemctl is-active php-fpm
active
curl -s -o /dev/null -w '%{http_code}\n' 'https://example.com/?r=1'
200

5️⃣ 패치 전 임시 조치: 워드프레스 원격 요청에서 br 풀기 끄기

Red Hat의 CVE 안내는 믿을 수 없는 곳에서 온 br 압축을 받지 않거나 자동으로 풀지 않도록 앱을 설정하라고 권합니다.

패치된 패키지가 나오기 전에도 워드프레스 쪽 경로는 지금 막을 수 있습니다. 워드프레스는 원격 요청마다 curl 핸들을 넘기는 [http_api_curl] 훅을 제공합니다. 거기서 [CURLOPT_ENCODING]을 null로 두면 libcurl의 자동 풀기가 꺼집니다.

아래 파일을 [wp-content/mu-plugins/] 폴더에 두면 모든 원격 요청에 적용되며, 파일 이름은 예시입니다. 되돌릴 때는 이 파일을 지우면 됩니다.

<?php
// no-brotli-decode.php — 워드프레스 원격 요청에서 libcurl 자동 풀기 끔(libbrotli 패치 버전이 나올 때까지)
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}
add_action( 'http_api_curl', function ( $handle ) {
    curl_setopt( $handle, CURLOPT_ENCODING, null );
}, 99 );

[CURLOPT_ENCODING]을 [gzip, deflate]로 줄이면 br이 빠진다고 생각하기 쉽습니다. 그 값은 요청 헤더의 Accept-Encoding에서 br을 빼는 것뿐이라, 상대 서버가 br로 답하면 libcurl은 받은 그대로 풉니다. 테스트 사이트에서 같은 값으로 httpbin.org/brotli를 받은 결과, Content-Encoding이 br인 응답이 그대로 풀려 읽혔습니다.

curl 문서 기준으로 자동 풀기를 끄는 값은 null뿐이며, null이면 Accept-Encoding 헤더를 보내지 않고 받은 본문도 풀지 않습니다. gzip·deflate 응답은 워드프레스의 Requests 라이브러리가 직접 풀어 그대로 읽히고, br 응답만 풀리지 않은 채 남습니다.

이 훅은 Requests가 curl을 실행하기 직전에 부르는 [curl.before_send] 자리에 워드프레스가 연결한 것입니다. Curl.php 174행에서 부르고 205행에서 실행하므로, 코어가 110행에서 넣은 빈 문자열 값이 그 사이에 덮어써집니다.

적용 여부는 아래 검사 파일로 확인합니다. httpbin.org는 받은 요청 헤더를 돌려주고 br·gzip으로 압축한 응답도 돌려주는 시험 주소라, 워드프레스가 무엇을 보내고 무엇을 푸는지 한 번에 보입니다.

<?php
// br-check.php — 워드프레스 원격 요청이 어떤 압축을 보내고 푸는지 확인(httpbin.org 시험 주소)
$h = json_decode( wp_remote_retrieve_body( wp_remote_get( 'https://httpbin.org/headers' ) ), true );
echo 'Accept-Encoding: ', isset( $h['headers']['Accept-Encoding'] ) ? $h['headers']['Accept-Encoding'] : '(none)', PHP_EOL;
foreach ( array( 'brotli', 'gzip' ) as $p ) {
    $r = wp_remote_get( 'https://httpbin.org/' . $p );
    $readable = strpos( wp_remote_retrieve_body( $r ), ': true' ) !== false;
    echo $p, ': Content-Encoding=', wp_remote_retrieve_header( $r, 'content-encoding' ), ' / ', strlen( wp_remote_retrieve_body( $r ) ), ' bytes / ', ( $readable ? 'readable' : 'not decoded' ), PHP_EOL;
}

파일을 넣기 전에는 Accept-Encoding에 br이 들어가고, br 응답이 풀려 읽힙니다.

wp eval-file br-check.php
Accept-Encoding: deflate, gzip, br
brotli: Content-Encoding=br / 316 bytes / readable
gzip: Content-Encoding=gzip / 317 bytes / readable

파일을 넣은 뒤에는 Accept-Encoding 헤더가 빠지고 br 응답은 풀리지 않은 채 남으며, gzip 응답은 그대로 읽힙니다.

wp eval-file br-check.php
Accept-Encoding: (none)
brotli: Content-Encoding=br / 199 bytes / not decoded
gzip: Content-Encoding=gzip / 272 bytes / readable

배포판의 패치된 libbrotli 패키지를 올리고 php-fpm 재시작까지 끝나면 이 파일을 지워 원래대로 되돌립니다.

🔢 FAQ & 추천 콘텐츠

워드프레스 자동 업데이트는 코어와 플러그인, 테마, 번역만 다루며, libbrotli와 Node.js는 서버에 설치된 패키지라 서버 관리자나 호스팅 업체가 올립니다. 올린 뒤 php-fpm 재시작까지 끝나야 워드프레스의 원격 요청이 새 라이브러리로 처리됩니다.

플러그인이 WordPress HTTP API를 거치든 curl 함수를 직접 호출하든 같은 libcurl을 지납니다. 고칠 것은 서버의 libbrotli 패키지이고, 패치 버전이 나오기 전에는 5️⃣의 설정으로 워드프레스 원격 요청의 자동 풀기를 끌 수 있습니다. curl 함수를 직접 호출하면서 [CURLOPT_ENCODING]을 설정하지 않은 코드는 curl 문서 기준으로 압축 응답을 자동으로 풀지 않습니다. 직접 호출하면서 빈 문자열로 설정한 코드는 [http_api_curl] 훅 밖이라 그 플러그인의 요청만 따로 확인합니다.

NVD와 cve.org의 CVE-2026-111497 기록(CNA 구글)에 결함 위치(c/dec/decode.c)와 영향 버전, 약점 유형이 있습니다. google/brotli 릴리스 v1.2.1·v1.1.1·v1.0.10 노트에는 고친 사실이 적혀 있습니다. 공격 재현 세부는 이 글에 넣지 않았습니다.

ℹ️ 제휴 안내
본 사이트의 콘텐츠에는 제휴 링크가 포함되어 있습니다. 방문자가 이 링크를 통해 상품 또는 서비스를 구매하면 본 사이트는 판매처로부터 수수료를 지급받습니다. 이 과정에서 구매자가 지불하는 금액(이벤트 할인 시 금액이 내려갑니다↓)은 오르지 않습니다. 게시된 가격·할인·재고 정보는 작성 시점 기준이며 실제와 다를 수 있으므로, 구매 전 판매처에서 최종 확인하시기 바랍니다. 상품 선정과 평가는 자체 기준에 따라 작성되며, 수수료 지급 여부가 소개 순서나 평가 내용에 영향을 주지 않습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

인간임을 증명하세요: 5   +   8   =