애플리케이션 코드 어디에도 import brotli가 없는데 취약한 Brotli 디코더가 실행될 수 있습니다. requests가 쓰는 urllib3와 httpx, aiohttp가 Brotli 패키지를 발견하면 응답을 자동으로 풀기 때문입니다. CVE-2026-111497이 Node.js뿐 아니라 Python 서비스의 점검 대상이 되는 이유입니다.

이 글은 Aikido의 Python 패키지 분석(2026-10-23 게시, 10-27 갱신)을 1차 근거로 삼았습니다. 이 분석은 보안 업체의 글이고 제품 홍보 성격이 섞여 있습니다. 10월 24~25일 GeekNews 화제는 발견 경로일 뿐입니다. 브리핑에서는 "HTTP2Shell"로 불렸는데, 이름은 여러 개(Node2Shell, Python2Shell 등)이며 HTTP/2 프로토콜과 관계가 없습니다. 추적할 식별자는 CVE 번호입니다.

확인된 것과 확인되지 않은 것

구분 내용
취약 버전 Brotli 1.2.0 이하(1.0.3부터 영향), brotlicffi 1.2.0.x
수정 버전 Brotli 1.2.1 이상, brotlicffi 1.2.1.0 이상
경로 외부 서버가 보낸 Brotli 응답을 앱이 받아 풀 때, 또는 Content-Encoding: br 요청 본문을 앱이 풀 때
CVSS CVE 기록 기준 4.0 9.2, 문서에 따라 Node.js는 high로 평가
재현 Aikido가 인용한 OSM 테스트에서 1.2.0은 약 23MB 조작 스트림에 계속 입력이 더 필요하다고 응답했고 1.2.1은 오류로 거부
미확인 Python 확장에서의 메모리 손상·코드 실행은 추적하지 않았다고 명시. Python용 PoC는 공개하지 않음. 실제 악용 사례도 Python에 대해서는 주장하지 않음

즉 "Python에서 원격 코드 실행이 입증됐다"고 쓸 수 없습니다. 입증된 것은 같은 디코더 코드가 Python 패키지에 들어 있고, 조작 입력에서 1.2.0이 비정상 상태에 빠진다는 점까지입니다. 반대로 "RCE가 없다"는 근거도 아닙니다. 불확실성 속에서 패치하는 쪽이 비용이 훨씬 낮습니다.

왜 내 코드에 없는데 영향받는가

Aikido의 설명에 따르면 urllib3(따라서 requests)는 Brotli 또는 brotlicffi가 설치돼 있을 때 Brotli 응답을 해제하고, httpx는 brotli extra로, aiohttp는 설치돼 있을 때 같은 일을 합니다. Scrapy 같은 상위 도구도 따라옵니다. 클라이언트는 Accept-Encoding: br를 보내고 응답을 자동 디코딩하므로, 웹훅 수신 후 URL 미리보기, 크롤러, 에이전트의 웹 검색·페치처럼 임의의 외부 서버에 접속하는 기능이 노출 경로입니다.

환경 점검 스크립트

아래는 제가 작성한 읽기 전용 점검 스크립트(brotli_audit.py)의 핵심입니다. 설치된 두 패키지의 버전을 수정 버전과 비교하고, 락파일에서 brotli를 찾습니다.

import importlib.metadata as md, re
FIXED = {"brotli": (1, 2, 1), "brotlicffi": (1, 2, 1, 0)}
def parse(v): return tuple(int(x) for x in re.findall(r"\d+", v)[:4])
for name, fixed in FIXED.items():
    try: v = md.version(name)
    except md.PackageNotFoundError:
        print(name, "not installed"); continue
    p = parse(v); p += (0,) * (len(fixed) - len(p))
    print(name, v, "OK" if p >= fixed else "VULNERABLE: upgrade")

제 로컬 Python 3.11.2에서 python3 -I로 실행한 결과는 아래와 같습니다. 이 환경에는 두 패키지가 없으므로 취약 사례를 재현한 것이 아닙니다.

brotli      -          not installed
brotlicffi  -          not installed
urllib3 default Accept-Encoding: gzip,deflate
lockfile sample hits: ['Brotli==1.1.0', 'httpx[brotli]==0.30']

세 번째 줄이 보여 주는 대로, Brotli가 없으면 urllib3의 기본 Accept-Encoding에서 br가 빠집니다. 취약한 패키지를 설치하는 순간 광고하는 인코딩이 늘어나는 셈이므로 실제 이미지에서 확인해야 합니다. Aikido가 제시한 확인 명령은 pip show Brotli brotlicffi, 컨테이너 이미지별 docker run --rm <image> pip show Brotli brotlicffi, pipdeptree -r -p Brotli입니다. 가상환경이 여러 개거나 이미지가 많다면 이미지 단위로 돌려야 합니다.

조치와 한계

  1. pip install -U "Brotli>=1.2.1" 또는 "brotlicffi>=1.2.1.0"로 올리고 락파일을 갱신해 이미지를 다시 빌드·배포합니다.
  2. 당장 올리기 어렵다면 Brotli 패키지와 httpx[brotli] extra를 제거할 수 있습니다. Aikido는 requests·urllib3·httpx가 gzip과 deflate는 유지한다고 하지만, 서버가 그래도 Brotli를 보내면 오류가 날 수 있습니다.
  3. 요청 본문(인바운드)은 엣지에서 Content-Encoding: br를 거부해 막을 수 있습니다. 앱이 가져오는 응답(아웃바운드)에는 소용이 없습니다. Accept-Encoding에서 br를 빼는 것만으로도 불충분하다고 합니다. 악의적 서버는 요청과 무관하게 Brotli를 보낼 수 있기 때문입니다.
  4. 제거는 다른 런타임이나 제품에 내장된 Brotli 디코더까지 없애지 않습니다. Node.js는 별도로 패치해야 하며 같은 글은 22.23.4, 24.22.1, 26.12.1을 안내합니다.

한 가지 주의할 점이 있습니다. 검색에서 확인한 다른 정리는 10월 23일 시점에 어드바이저리 DB가 두 PyPI 패키지에 CVE를 연결하지 않아 pip-audit·Dependabot·Safety가 잡지 못할 수 있다고 적었고, Aikido 글은 10월 27일 갱신본에서 PYSEC 식별자를 언급합니다. 도구의 최신 DB 반영 여부는 직접 확인해야 하며, 스캐너 결과가 비어 있다고 안전하다고 결론 내리면 안 됩니다. 이 글에서 CVE 기록(NVD 등)과 Brotli 저장소의 수정 커밋은 직접 대조하지 않았습니다.

개발자 관점의 결론

전이 의존성은 "쓰지 않는다"가 아니라 "누가 대신 쓰는가"로 점검해야 합니다. 에이전트가 임의의 URL을 가져오는 서비스라면 외부 응답의 디코딩은 신뢰 경계 안쪽에서 일어납니다. 우선순위는 임의 외부 서버에 접속하는 서비스, 그다음 요청 본문을 압축 해제하는 서비스입니다. 내부 API만 호출하는 배치는 뒤로 미뤄도 되지만, 이미지에 Brotli가 설치되어 있는지부터는 확인하세요.