3P by GN⁺ | ★ favorite | 댓글 1개
  • HTTP/2 CONTINUATION FloodEND_HEADERS 없이 헤더 프레임을 계속 보내 서버 가용성을 무너뜨릴 수 있는 HTTP/2 구현 취약점 계열임
  • 공격 요청이 완성되지 않아 HTTP 접근 로그에 남지 않으며, 원인 파악에는 원시 트래픽 바이트 분석이 필요할 수 있음
  • 구현에 따라 CPU 고갈, 다중 연결 OOM, 단일 연결 OOM, 연결 해제 타이밍 버그로 인한 크래시까지 영향 범위가 달라짐
  • Go, Firefox, Node.js 사례에서는 각각 HPACK 디코딩 지속, 응답 헤더 크기 제한 누락, CONTINUATION 처리 중 연결 해제와 메모리 카운터 갱신 충돌이 드러남
  • Rapid Reset과 달리 많은 구현에서 단일 TCP 연결만으로도 서버 크래시가 가능해, HTTP/2를 쓰는 광범위한 인터넷 서비스가 영향을 받을 수 있었음

HTTP/2에서 CONTINUATION 프레임이 쓰이는 방식

  • HTTP/2는 HTTP/1.1처럼 텍스트 라인을 주고받지 않고 바이너리 프레임을 교환하는 프로토콜임
  • HEADERS 프레임은 요청과 응답의 HTTP 헤더를 보내며, 헤더 데이터는 HPACK으로 인코딩된 field block fragment에 저장됨
  • HEADERS 프레임에는 헤더와 스트림의 끝을 알리는 플래그가 있음
    • END_HEADERS: 해당 프레임이 보내려는 모든 헤더를 포함한다는 신호
    • END_STREAM: 요청 또는 응답 본문이 더 없다는 신호
  • 프레임에는 통신 시작 시 정해지는 최대 크기가 있고, 수신 프레임이 허용 크기를 넘으면 프로토콜 오류로 연결이 끊김
  • 단일 HEADERS 프레임에 헤더를 모두 담을 수 없으면 END_HEADERS 없는 HEADERS 뒤에 CONTINUATION 프레임들이 이어짐
    • END_HEADERS 없는 HEADERS 프레임
    • END_HEADERS 없는 추가 CONTINUATION 프레임들
    • 마지막 CONTINUATION 프레임에 END_HEADERS 설정
  • 마지막 헤더 프레임 뒤에는 요청 데이터를 담는 DATA 프레임이 오거나 HTTP/2 스트림이 끝남

취약점의 핵심: 끝나지 않는 헤더 스트림

  • 클라이언트가 새 HTTP/2 스트림을 시작한 뒤 HEADERSCONTINUATION 프레임을 보내면서 END_HEADERS를 절대 설정하지 않으면, 서버는 무한 헤더 스트림을 계속 파싱하고 저장하려 함
  • HTTP/1.1 서버는 일반적으로 무한 헤더를 막는 두 가지 장치를 갖고 있음
    • 헤더 목록이 허용 크기를 넘으면 연결을 끊는 헤더 크기 제한
    • 요청이나 헤더가 제때 전송되지 않으면 연결을 끊는 요청/헤더 타임아웃
  • 여러 HTTP/2 구현에서는 이런 보호가 없거나 잘못 구현되어 있었고, Apache httpd, Envoy, 여러 HTTP/2 패키지와 코덱도 포함됨
  • 구현별 결과는 네 갈래로 나뉨
    • CPU 고갈: 추가 헤더를 읽고 디코딩하는 동안 CPU 사용량이 늘어 다른 요청 응답이 느려지거나 막힘
    • 다중 연결 기반 OOM: CONTINUATION 헤더가 메모리에 저장되고, 헤더 목록 크기 제한은 있지만 헤더 타임아웃이 없어 각 연결이 메모리를 계속 점유함
    • 단일 연결 기반 OOM: 일부 구현은 메모리가 찰 때까지 헤더를 계속 읽어 OS가 프로세스를 종료하게 됨
    • 몇 개 프레임만으로 크래시: CONTINUATION 스트림 중간에 연결이 끊길 때 구현 버그로 서버가 크래시됨
  • END_HEADERS가 없으면 요청이 제대로 닫히지 않으므로 악성 클라이언트의 요청은 접근 로그에 저장되지 않음

Go 사례: HPACK 디코딩이 멈추지 않는 CPU 고갈

  • Go는 CONTINUATION Flood에서 CPU 고갈이 두드러진 사례임
  • Go 구현은 http2MetaHeadersFrame 추상화로 하나의 HEADERS 프레임, 0개 이상의 CONTINUATION 프레임, HPACK 디코더를 묶음
  • readMetaFrame은 헤더 크기 제한에 도달하거나 오류가 발생하면 SetEmitEnabled(false)를 호출해 디코딩된 헤더 방출을 멈춤
  • 하지만 헤더 방출을 멈춘 뒤에도 HPACK 디코더는 입력 바이트를 계속 디코딩함
  • 프레임 공급 루프는 HeadersEnded()true가 될 때만 멈추며, 이는 END_HEADERS 플래그가 설정될 때 발생함
  • 공격자가 END_HEADERS를 보내지 않으면 readMetaFrame은 반환되지 않고, 공격자가 보내는 동안 HPACK 디코더가 계속 새 바이트를 처리함

OOM 사례와 Firefox 클라이언트 영향

  • OOM은 CONTINUATION 프레임으로 만들어지는 헤더 목록 크기를 제한하지 않은 구현에서 발생함
  • 헤더 타임아웃이 없는 구현에서는 단일 HTTP/2 연결만으로 서버를 크래시시킬 수 있었음
  • idle timeout이 있는 구현에서도 여러 HTTP/2 연결이 연결별 한도에 가까운 RAM을 점유하게 만든 뒤, 마지막 CONTINUATION 프레임을 몇 초마다 바이트 단위로 보내 연결을 유지하는 방식이 가능했음
  • CONTINUATION Flood는 서버뿐 아니라 브라우저 같은 클라이언트 측에서도 발생할 수 있음
  • Mozilla Firefox 수정 커밋에는 집계된 헤더 크기와 새 프레임 크기의 합이 network_http_max_response_header_size()를 넘으면 PROTOCOL_ERROR로 세션 오류를 반환하는 검사가 추가됨

Node.js 사례: 연결 해제 중 assertion 크래시

  • Node.js는 무한 CONTINUATION 프레임 스트림 자체는 적절히 처리했지만, 헤더 스트림 중 연결이 끊길 때 data race가 발생함
  • 공격 코드 실행 중 Node.js는 Http2Session::~Http2Session()에서 CHECK_EQ(current_nghttp2_memory_, 0) assertion 실패로 크래시함
  • 크래시는 HTTP/2 클라이언트가 Node.js 서버에서 연결을 끊는 정확한 시점과 연결되어 있었고, assertion은 Http2Session 소멸자 안에 있었음
  • Node.js는 HTTP/2 연결 처리를 위해 nghttp2 라이브러리를 내장함
  • current_nghttp2_memory_는 nghttp2 내부에서 할당된 메모리를 추적하며, 소멸자의 session_.reset() 이후 모든 nghttp2 아티팩트가 메모리에서 제거됐는지 확인함
  • 조사 결과 CONTINUATION 프레임 파싱 중 nghttp2 콜백과 reset()이 함께 실행되는 경우가 있었음
    • NGHTTP2_IB_EXPECT_CONTINUATION 상태에서 CONTINUATION 프레임이 도착함
    • 상태가 NGHTTP2_IB_READ_HEADER_BLOCK으로 바뀜
    • session_after_header_block_received, session_call_on_frame_received, on_frame_recv_callback 흐름이 이어짐
    • Node.js의 OnFrameReceiveHandleHeadersFrame에서 메모리 카운터를 갱신함
  • HandleHeadersFrameHttp2Session::~Http2Session()이 동시에 실행되면 current_session_memory_가 동시에 업데이트되고, current_nghttp2_memory_ 값이 음수가 되어 CHECK_EQ가 실패함

2019년 HTTP/2 취약점들과의 차이

  • 2019년 Netflix와 Google이 보고한 HTTP/2 취약점 묶음은 CERT/CC Vulnerability Note VU#605641에 정리되어 있음
  • CVE-2019-9516, “0-Length Headers Leak”은 길이 0의 헤더 이름과 값으로 일부 구현이 메모리를 할당하고 세션이 끝날 때까지 유지하게 만드는 문제임
  • CONTINUATION Flood는 빈 헤더가 아니라 서버가 설정한 프레임 크기 제한까지 많은 무작위 헤더를 보내는 방식임
  • CVE-2019-9518, “Empty Frame Flooding”은 빈 payload의 DATA, HEADERS, CONTINUATION, PUSH_PROMISE 프레임 등을 end-of-stream 없이 보내 상대가 공격 대역폭에 비해 과도한 처리 시간을 쓰게 만드는 문제임
  • CONTINUATION Flood는 빈 프레임이 아니라 가능한 한 큰 프레임을 사용해 메모리를 점유하고 디코딩 과정에서 CPU 사이클을 소비함

Rapid Reset보다 더 심각할 수 있었던 이유

  • 2023년 10월 HTTP/2 프로토콜의 zero-day인 “Rapid Reset” 세부 내용이 공개됐고, “현재까지 가장 큰 DDoS 공격”으로 불렸음
  • Rapid Reset은 END_STREAMEND_HEADERS가 설정된 HEADERS 프레임과 RST_STREAM 프레임 조합을 사용함
  • 이 방식에서는 rate limiting 같은 표준 완화책이 피해를 줄일 수 있고, 서버 관리자는 많은 인바운드 요청을 로그에서 보고 경고를 받을 수 있음
  • CONTINUATION Flood에서는 END_HEADERS가 없으므로 단 하나의 요청도 완성되지 않으며, 관리자는 로그에서 요청을 볼 수 없음
  • 많은 구현에서 CONTINUATION Flood는 단일 TCP 연결만으로 서버 크래시가 가능했고, 일부 사례는 아주 적은 데이터만으로도 가능했음
  • Rapid Reset은 DDoS 공격에 사용됐고, 대부분의 경우 성공적인 공격에 botnet이 필요했음

인터넷 서비스에 미칠 수 있었던 영향

  • Cloudflare Radar에 따르면 HTTP/2 트래픽은 봇을 제외한 인간 HTTP 트래픽의 약 60% 를 차지함
  • Cloudflare Radar는 HTTP 트래픽이 전체 인터넷 전송의 70% 이상이라고 추정함
  • 영향받는 프로젝트의 중요성과 쉬운 악용 가능성을 고려하면 인터넷의 큰 부분이 이 취약점에 노출되어 있었음
  • HTTP는 웹사이트뿐 아니라 많은 RESTful API에도 사용됨
  • 중요한 기업·정부 API와 웹사이트의 가용성 문제는 수백만 달러 손실이나 혼란을 만들 수 있음
  • 악용됐다면 서버 관리자가 HTTP/2 지식 없이 디버깅하기 매우 어려웠을 수 있음
    • 악성 HTTP 요청이 제대로 닫히지 않음
    • 서버 접근 로그에 요청이 보이지 않음
    • 대부분의 HTTP/2 서버에는 고급 프레임 분석 기능이 부족함
    • 수동으로 원시 연결 데이터를 분석해야 함

조정 공개와 CERT/CC 대응

  • 이 취약점 계열은 인터넷 안전에 상당한 위험을 가졌음
  • 2024년 1월 보고 이후 CERT/CC는 이 문제를 추적하기 위해 Vulnerability Coordination case를 열었음
  • 여러 기술 대기업과 오픈소스 프로젝트가 관련된 책임 있는 공개 과정에 참여함
  • 단일 연구자가 많은 구현을 모두 점검하기 어렵기 때문에, 여러 벤더에 영향을 주는 문제에는 Vulnerability Coordination이 필요했음
  • CERT/CC는 이 문제에 대한 Vulnerability Note를 공개했으며, 이런 노트는 매년 몇 개만 공개됨

댓글과 토론

Hacker News 의견들
  • 지난달에 Bandit에서 바로 이 문제를 완화했었음
    https://github.com/mtrudel/bandit/blob/main/lib/bandit/http2...
    구현자 관점에서는 솔직히 너무 당연히 막아야 하는 부분임. 오래전부터 신경 쓰고 있었고, 다른 구현들도 당연히 방어하고 있을 거라고 생각해 왔음

    • 우리가 가정하면 무슨 일이 생기는지 알잖음. 너와 나를 프런트 페이지 헤드라인으로 만들게 됨
  • 지난 몇 달 동안 수십 개 구현을 확인했는데, 이상하게도 주요 HTTP/2 서버들조차 이런 보호가 없거나 잘못 구현돼 있었음
    본질적으로는, 모든 것을 자동으로 동적으로 확장하는 데 익숙하고 크기가 얼마나 커질 수 있는지 신경 쓰지 않는 개발 문화가 만든 결과라고 봄
    이 부류의 문제가 HTTP/2에만 국한되지는 않지만, HTTP/2의 심한 복잡성이 한몫했을 가능성은 큼. HTTP/1.x 시절에는 C 같은 언어에 익숙한 개발자가 더 많아서 버퍼 길이 관리에 계속 신경 썼고, 요청 전체에서 헤더 할당은 많아야 몇 KB면 충분한데도 무한히 늘어나게 만들지는 않았을 것임

    • 사람들은 계속 정상 경로에만 집중하고 최적화하지만, 공격자가 최악의 상황을 의도적으로 반복해서 유발하면 어떻게 되는지는 멈춰서 생각하지 않음
      slowloris, 쿼리 매개변수 해시 충돌 같은 많은 서비스 거부 공격은 제한된 자원 사용량을 나중에야 고려해서 현실화됨
  • > 영향 없음: Nginx, Jetty, HAProxy, NetScaler, Varnish. [0]
    0: https://nowotarski.info/http2-continuation-flood/

    • 다시 말해, 10년 전부터 서비스 거부 위험 때문에 CONTINUATION 사용에 반대해 오던 구현들임. 긴 스레드들을 읽어보면 항상 골치 아픈 CONTINUATION을 어떻게 피할지가 핵심이었음: https://lists.w3.org/Archives/Public/ietf-http-wg/2014JulSep...
      최소한 꽉 차지 않은 HEADERS 프레임 뒤에는 금지하는 안이라도 받아들여졌다면 더 견고했을 텐데, 인코딩 작업 자체가 더 어려워질 수 있다고 여겨졌음. 압축기의 바이트 경계 같은 문제 때문임
      10년마다 같은 것들을 “재발견”하는 모습이 웃김. 최근에는 잘 알려진 RESET_STREAM flood였고, 이번에는 CONTINUATION이고, 곧 길이 0인 DATA 프레임, 1바이트 WINDOW_UPDATE, CPU를 많이 쓰게 만드는 INITIAL_WINDOW SETTINGS 같은 것들이 나올 듯함. 알려진 문제에 이름과 가능하면 로고만 붙일 수 있으면, 세상은 이 보안 서커스를 계속 돌고 있음
    • Caddy는 어떤가? 훌륭한 프로젝트라서 따로 한 줄 받을 만함 ;)
  • 같은 작성자가 영향받는 웹 서버/리버스 프록시를 정리한 이전 글
    https://nowotarski.info/http2-continuation-flood/

  • 이 글이 하루 종일 최상단에 있었음
    궁금한데, 트래픽이 적은 웹사이트라면 그냥 HTTP/1.1로 운영하는 편이 더 안전할 수도 있나?

    • HTTP/1.1은 구현하기 훨씬 쉬우므로 버그가 더 적을 거라고 보는 게 합리적임
      HTTP/2와 HTTP/3는 기능 면에서 크게 다름. 다중화, 윈도잉, HPACK 등이 추가되면서 HTTP/1.1의 거의 무상태 연결이 상태 있는 연결로 바뀜. 상태 있는 연결을 유지하려면 상태와 설정 같은 데이터를 저장해야 하고, 그래서 이런 문제들이 생김
      HTTP/2에서는 다중화가 추가되므로 방어 특성도 달라짐. 예를 들어 연결이 CDN 원본 요청에서 온다면 연결 수는 적게 허용하고 각 연결에 큰 다중화 채널 풀을 둘 수 있지만, 직접 사용자 접근이라면 연결 수는 많이 허용하되 각 연결의 다중화 채널 수는 줄이고 싶을 수 있음. HTTP/1에서는 거의 모두가 비슷해 보였기 때문에 방어가 훨씬 단순했음
    • 단지 업그레이드하기 위해 업그레이드하는 것은 좋은 엔지니어링 관행이 아님. 업그레이드로 추가 이익이 없다면 정당화되기 어려움
    • 꼭 그렇지는 않음. HTTP/1.1이 단순하다는 사람들은 실제 환경과 호환되는 완전한 파서를 구현해 본 적이 없는 것임
      HTTP/1에는 눈에 잘 띄지 않는 경계 조건과 오래된 예외 동작이 많음. 텍스트 형식은 유효한 헤더만 볼 때보다 훨씬 유연하고, 여러 줄 헤더, 오래된 MIME 기능, 100-continue 경쟁 조건, 사용자 정의 홉별 헤더, GET 본문 같은 애매한 기능도 있음
      다행히 새 HTTP RFC들은 많은 함정을 문서화하고 있음. RFC 2616만 보고 구현하면 안전한 구현이 되지 않음
      요청이나 응답의 실제 크기는 여러 방식으로 동시에 지정될 수 있고 값이 충돌할 수도 있음. 또한 여러 기능 조합과, 하위 호환을 위해 이상한 파싱 규칙이 필요한 헤더 값에 따라 달라져서, “단순한” HTTP 구현은 요청 밀수에 속을 수 있음
      어느 쪽이든 견고하고 잘 테스트된 성숙한 구현이 필요함
    • 나도 그게 궁금했음. 더 성숙하고 덜 복잡하니 더 안전할 가능성이 있어 보임
    • 아마도 그럴 것임. HTTP/2는 스트리밍에 좋고, 그마저도 더 새로운 프로토콜로 대체되고 있음
      일반적인 정적 자산 제공에서는 HTTP/1이 도메인당 연결 수 제한을 받기 때문에 더 많은 자산을 병렬로 불러올 수 있다는 점만 장점임. 서로 다른 도메인의 CDN을 쓰면 보통 이 문제도 막을 수 있음
      이론상 HTTP/2로 번들링하지 않은 JavaScript 자산을 제공할 수 있지만, 실제 운영 환경에서는 본 적이 없음. 대부분의 경우 여전히 컴파일 단계가 필요하기 때문일 가능성이 큼
  • 이걸 천천히 하면 slowloris v2라고 부를 수 있겠네 :(

  • HTTP/2, 또는 전송 계층 “업그레이드”를 애플리케이션 계층 프로토콜에 억지로 끼워 넣는 법