Daily-It

Java·백엔드 운영 경험으로 AI·개발·자동화와 일상 IT의 실제 판단을 기록하는 기술 블로그입니다.

HTTP2Shell Brotli 취약점, Node.js·Python·.NET 수정 버전과 점검 순서

HTTP2Shell을 점검할 때는 웹서버의 HTTP/2 설정보다 외부 URL을 읽는 배치·API와 압축된 요청 본문을 받는 기능을 먼저 살펴야 한다. 이 취약점(CVE-2026-111497)은 Brotli 압축을 푸는 디코더에서 생기기 때문이다. 아래는 Node.js, Python Brotli 바인딩, .NET의 수정 버전과 확인 명령, 바로 배포하지 못할 때의 임시 차단 방법을 정리한 점검표다. 취약점을 직접 재현한 기록은 아니며, 임시 차단은 업데이트를 대신하지 못한다.

목차

무엇부터 확인해야 하나?

외부에서 온 Brotli 데이터가 어디서 압축 해제되는지부터 본다. 경로는 두 가지다. 서비스가 외부 URL을 호출해 받은 Brotli 응답을 푸는 경우와, 클라이언트가 보낸 Content-Encoding: br 요청 본문을 서버가 푸는 경우다. 이름에 HTTP/2가 들어 있지만 HTTP/2 전용 문제는 아니며, 취약한 Brotli 디코더로 공격자가 제어하는 데이터를 푸는 경로가 점검 대상이다.[1]

CVE-2026-111497은 Google Brotli C 디코더의 범위 밖 쓰기(out-of-bounds write) 취약점이다. Node.js 공지는 프로세스 크래시, 메모리 노출, 원격 코드 실행으로 이어질 수 있다고 설명하고, Microsoft는 정보 유출·서비스 거부·권한 상승 가능성을 밝혔다.[2][8] 심각도 표기는 출처마다 다르다. Node.js와 Microsoft는 High(Microsoft 기준 CVSS 3.1 8.1)로, Google의 CVE 등록 정보는 CVSS 4.0 9.2(Critical)로 표기한다. 서로 다른 제품·평가 버전의 점수를 하나의 등급으로 섞어 읽지 않는다.[2][8][14]

BleepingComputer는 2026년 10월 26일 보도에서 PoC 공개 사실과 당시 실악용 보고가 없다는 점을 함께 전했다. 이는 이후의 악용이 없다는 보장은 아니다.[1] Imperva는 공식 Node.js 24.22.0 Linux arm64 이미지에서 코드 실행을 재현했고 24.22.1에서는 같은 입력이 거부됐다고 밝혔다. 이 결과는 해당 환경의 연구 재현이지 모든 운영 환경에서 같은 방식으로 공격된다는 뜻은 아니다.[9] 이 글에는 PoC 코드나 배포 링크를 싣지 않는다.

점검 순서는 현재 버전 기록, 압축 해제 경로 확인, 수정 버전 적용, 배포 후 재확인이다. 인터넷에서 요청을 받는 서버뿐 아니라 외부 응답을 읽는 클라이언트도 목록에 넣는다.

  • 외부 URL 조회·웹훅·크롤러·링크 미리보기처럼 외부 응답을 푸는 기능
  • Content-Encoding: br 요청 본문을 푸는 미들웨어(Express 5, body-parser 2 등)
  • 운영 컨테이너·가상환경·CI 러너를 각각 따로 점검
  • 업데이트 전후 버전과 배포 이미지 식별자 기록
왼쪽 서버의 응답과 오른쪽 노트북의 압축 요청이 가운데 Brotli 디코더로 들어가는 개념 일러스트
외부 응답과 압축 요청이 같은 Brotli 디코더로 들어가는 두 경로 개념 일러스트(daily-it.com 자체 제작)

어느 버전으로 올려야 하나?

각 계열에서 아래 표의 수정 버전 이상으로 올린다. Node.js는 공식 릴리스 노트, Python 패키지는 PyPA 보안 공지, .NET은 Microsoft 보안 공지와 대조했다.[3][6][7][8]

대상영향 버전수정 버전확인 방법
Node.js22.x·24.x·26.x의 수정판 이전 버전
지원 종료(EOL) 계열은 지원 계열로 이전
22.23.4 [3]
24.22.1 [4]
26.12.1 [5]
node --version
Python Brotli1.2.1 미만1.2.1 [6]pip show Brotli
Python brotlicffi1.2.1.0 미만1.2.1.0 [7]pip show brotlicffi
.NET 8·9·108.0.0 이상 8.0.33 미만
9.0.0 이상 9.0.22 미만
10.0.0 이상 10.0.14 미만
8.0.33
9.0.22
10.0.14 [8]
dotnet --list-runtimes
.NET 11 프리뷰RC3 이전 프리뷰·RC11.0.0-rc.3.26522.148 [8]dotnet --list-runtimes
Google Brotli C 라이브러리1.0.x: 1.0.3 이상 1.0.10 미만
1.1.x: 1.1.1 미만
1.2.x: 1.2.1 미만
1.0.x → 1.0.10
1.1.x → 1.1.1
1.2.x → 1.2.1 [11][14]
배포판 패키지·번들 사본의 버전

Python 행은 requests·httpx 자체의 버전이 아니다. requests는 urllib3를 거쳐 설치된 Brotli 바인딩을 쓰고, httpx도 Brotli 지원을 설치한 환경에서 압축 응답을 자동으로 푼다. httpx[brotli]는 그 지원을 설치하는 방법이다. 따라서 HTTP 클라이언트만 올리지 말고 실제로 설치된 Brotli·brotlicffi의 버전을 확인한다.[6][7]

.NET 11 프리뷰를 쓰고 있다면 RC3(11.0.0-rc.3.26522.148)로 올린다. 영향 시작점이 패키지마다 preview.1 또는 11.0.0-preview.1.26104.118로 달라서, 런타임 팩이나 ILCompiler를 직접 참조한다면 공지의 패키지 표와 맞춰 본다. .NET Framework는 영향이 없다.[8]

Google Brotli C 라이브러리는 계열별로 1.0.10·1.1.1·1.2.1에서 수정됐다. 표의 영향 범위는 Google이 등록한 CVE의 계열별 범위를 따른다.[14] Python 패키지 번호와 비슷해 보여도 별개이니 섞어 쓰지 않는다. Node.js는 업스트림 수정 커밋을 체리픽해 넣었으므로, 내장 Brotli 버전 문자열(process.versions.brotli)이 아니라 Node.js 버전으로 판단한다.[3][11]

Node.js는 npm 업데이트로 끝나나?

아니다. 취약한 디코더가 Node.js 런타임 안에 들어 있어서 Node.js 자체를 수정 버전으로 바꿔야 한다.[2][3] Express 5나 body-parser 2는 Brotli 요청 본문을 디코더로 넘기는 경로일 뿐이라 이 패키지들을 올려도 해결되지 않는다.[9] 시스템 Brotli 라이브러리를 업데이트해도 Node.js가 내장한 사본은 그대로 남는다.[1]

버전은 개발 터미널이 아니라 서비스가 실제로 도는 컨테이너나 실행 계정에서 확인한다. 한 서버에 Node.js가 여러 벌 설치돼 있을 수 있으니 실행 경로도 함께 본다.

node --version
node -p 'process.execPath'

# Running service container
docker exec "$CONTAINER" \
  node --version

아래 명령은 환경에 맞게 선택하는 예시이며, 이 글에서 운영 서버에 실행해 검증한 기록은 아니다. $CONTAINER에는 실제 서비스 컨테이너 이름을 넣는다. nvm을 쓰는 22.x 환경이라면 아래처럼 올린다. 24.x·26.x는 버전 번호만 표의 값으로 바꾼다. 서비스 관리자나 컨테이너가 다른 경로의 Node.js를 쓰면 그쪽 설치본과 베이스 이미지도 따로 교체해야 한다.

nvm install 22.23.4
nvm use 22.23.4
node --version
  • 서비스 실행 경로의 Node.js 교체
  • 컨테이너 베이스 이미지 태그 갱신 후 재빌드
  • 프로세스 재시작 후 실제 실행 버전 재확인
  • 지원 종료 계열은 지원 계열로 이전

Node.js 공지는 보안 릴리스가 나오면 지원 종료(EOL) 버전은 항상 영향을 받는다고 명시한다. 18.x·20.x처럼 표에 없는 계열은 안전한 것이 아니라 공식 수정판이 없는 것이다.[2][12]

Python은 무엇을 업데이트하나?

가상환경이나 이미지에 설치된 Brotli·brotlicffi 패키지를 올린다. 코드에서 직접 import하지 않아도 requests(urllib3)·httpx·aiohttp가 설치된 바인딩을 알아서 쓰기 때문에, 설치 여부와 외부 응답 처리 경로를 함께 점검해야 한다.[6][7] 확인은 서비스가 쓰는 것과 같은 인터프리터로 한다.

python -m pip show requests httpx
python -m pip show Brotli
python -m pip show brotlicffi

pip show의 Required-by는 참고 정보다. 선택적 extra로 들어온 의존성이 모두 표시되는 것은 아니므로, 설치 설정·잠금 파일·의존성 트리도 함께 확인한다. 업데이트는 이미 설치된 패키지에만 한다. 점검하려고 없던 바인딩을 새로 설치할 필요는 없다.

python -m pip install -U \
  'Brotli>=1.2.1'
python -m pip install -U \
  'brotlicffi>=1.2.1.0'
python -m pip check
  • requirements·잠금 파일(poetry.lock, uv.lock 등)의 버전 제약 갱신
  • 깨끗한 환경에서 다시 설치해 버전 재확인
  • 운영 이미지 재빌드·배포, 워커 재시작
  • 배포판 패키지(python3-brotli 등)로 설치했다면 배포판 보안 공지 기준으로 판단

로컬 가상환경만 올리고 예전 이미지로 배포하면 운영 환경은 그대로다. 배포판 패키지는 업스트림 버전 번호를 그대로 두고 수정만 백포트하기도 하므로, PyPI 버전 번호와 비교해 판단하지 않는다.[12]

.NET은 서버 런타임만 바꾸면 되나?

프레임워크 종속 배포는 수정 런타임을 설치한 뒤 앱을 재시작한다. 설치 목록에 새 버전이 보이는 것만으로 끝내지 말고, 앱의 runtimeconfig.json·롤포워드 설정과 실행 로그를 통해 실제 선택된 런타임도 확인한다. 자체 포함(self-contained) 배포와 NativeAOT는 앱에 런타임이나 디코더가 함께 들어가 있어서 수정된 SDK로 다시 빌드해 배포해야 하고, 컨테이너도 수정된 .NET 이미지로 다시 빌드한다.[8][13]

dotnet --list-runtimes
dotnet --list-sdks

운영 서버는 원래 쓰던 설치 방식(패키지 관리자나 Microsoft 설치 프로그램)으로 업데이트한다. 아래는 Linux/macOS CI에서 이미 공식 설치 스크립트를 쓰고 있을 때 .NET 10 SDK를 올리는 예시다. SDK 10.0.403은 수정 런타임 10.0.14에 대응한다. Microsoft는 이 스크립트를 CI용으로 안내하므로 운영 서버 업데이트에는 쓰지 않는다.[8][10]

bash ./dotnet-install.sh \
  --version 10.0.403 \
  --install-dir "$HOME/.dotnet"
"$HOME/.dotnet/dotnet" \
  --list-runtimes
  • global.json이 예전 SDK를 고정하고 있지 않은지 확인
  • 직접 참조한 런타임 팩·ILCompiler 패키지 버전 갱신
  • 자체 포함·NativeAOT 앱은 수정 SDK로 다시 게시(publish)
  • 컨테이너는 수정된 .NET 이미지로 재빌드·재배포
  • 재시작 후 앱이 실제로 선택한 런타임 확인

NativeAOT는 디코더가 실행 파일 안에 링크돼 있어서, 서버의 공유 런타임을 바꿔도 이미 빌드된 실행 파일은 그대로다. 빌드 환경의 SDK와 명시적으로 참조한 컴파일러·런타임 팩을 올린 뒤 다시 게시해야 한다.[8]

바로 배포할 수 없다면 어떻게 막나?

취약한 디코더에 외부 Brotli 데이터가 들어가지 않도록 압축 해제 경로를 잠시 막는다. 어디까지나 업데이트 전까지의 임시 조치다.

  • 수신 요청: 리버스 프록시·로드밸런서·미들웨어에서 본문을 풀기 전에 Content-Encoding에 br이 들어간 요청을 거부한다. 대소문자와 여러 인코딩을 나열한 값도 걸러야 한다. 서버의 응답 압축을 끄는 것과는 다른 조치다.[12]
  • 외부 응답: 자동 Brotli 해제를 끄거나, 디코딩이 시작되기 전에 Content-Encoding: br 응답을 거부한다. 둘 다 어렵다면 해당 조회 기능을 잠시 멈춘다.[12]
  • Accept-Encoding에서 br만 빼는 것으로는 부족하다. 악성 서버는 요청 헤더와 관계없이 Brotli 응답을 보낼 수 있다.[12]

Node.js 내장 fetch()는 Brotli 응답을 자동으로 풀고, 이를 끄는 표준 설정이 없다. 따라서 요청 헤더만 바꿔 안전하다고 판단하지 않는다. 즉시 업데이트할 수 없다면 신뢰할 수 없는 URL 조회 기능을 중단하거나 해당 경로를 격리해 디코더에 도달하지 못하게 한다. 이 조치는 수정 런타임 배포 전까지의 임시 대응이다.[12]

Python에서 Brotli가 꼭 필요하지 않다면 바인딩을 지우는 것도 임시 방법이다. 바인딩이 없어지면 클라이언트의 Brotli 지원이 달라지므로, 계속 Brotli로 응답하는 서버와의 연동은 실패할 수 있다. 다른 압축 방식의 지원 여부도 각 클라이언트의 설치 구성에 따라 확인한다. httpx[brotli] 같은 extra와 잠금 파일도 정리하지 않으면 다음 설치 때 다시 들어온다.[12]

python -m pip uninstall \
  Brotli brotlicffi
python -m pip check

.NET은 이미 Brotli 압축 해제를 쓰고 있는 경우에만 설정을 고친다. 기본 설정에서는 ASP.NET Core 요청 압축 해제와 HttpClient 자동 응답 해제가 꺼져 있으므로, 완화한다고 새로 켜지 않는다. Microsoft는 Brotli를 빼더라도 자동 압축 해제를 새로 켜면 서비스 거부 위험이 생길 수 있다고 경고한다. 기존 요청 압축 해제 설정에서는 br 공급자를 뺀다.[8]

builder.Services
  .AddRequestDecompression(o =>
  {
      o.DecompressionProviders
          .Remove("br");
  });

이미 AutomaticDecompression을 지정한 HttpClient라면 Brotli(또는 All)를 빼고 필요한 방식만 남긴다. BrotliStream·BrotliDecoder를 직접 쓰는 코드는 신뢰할 수 없는 데이터를 넘기지 않도록 막는다. 바꾼 뒤에는 정상 요청·응답 처리를 테스트한다.[8]

var handler = new HttpClientHandler
{
    AutomaticDecompression =
        DecompressionMethods.GZip |
        DecompressionMethods.Deflate
};

언제 점검을 끝내도 되나?

설치 명령의 성공이 아니라 운영 배포물의 교체와 정상 동작 확인을 완료 기준으로 삼는다. 아래 항목을 서비스별로 기록하면 개발 환경만 업데이트하고 운영 이미지나 워커를 빠뜨리는 일을 줄일 수 있다.

체크리스트를 하나씩 확인하며 이전 패키지 묶음을 새 묶음으로 교체하는 모니터 화면 개념 일러스트
업데이트 전후를 확인하며 배포물을 교체하는 점검 개념 일러스트(daily-it.com 자체 제작)
  • 운영 컨테이너·가상환경에서 수정 버전 확인
  • 이전 이미지와 재시작하지 않은 워커 정리
  • 정상적인 압축 응답·요청과 주요 기능 회귀 테스트
  • 임시 차단으로 늘어난 연동 실패·오류율 점검
  • 롤백용 이미지도 수정 버전으로 교체

압축 해제 중 예상치 못한 프로세스 종료나 디코더 오류는 조사 우선순위를 정하는 데 참고할 수 있지만, 그것만으로 공격 여부를 판단할 수는 없다.[9] 공개 PoC를 운영 서비스에 보내 확인하는 일도 하지 않는다. 지금 할 일은 공격이 통하는지 시험하는 것이 아니라, 외부 입력을 푸는 디코더와 그 디코더가 들어 있는 배포물을 바꾸는 것이다.

참고 자료