flawopen.com/Teardowns/cve-2026-71416-headroom-websocket-origin-hijacking
CVE-2026-71416: Headroom WebSocket Origin 하이재킹
Headroom은 WebSocket Origin을 확인하지 않아 방문한 사이트가 프록시로 LLM 요청을 보낼 수 있었습니다. 0.35.0 이상으로 업데이트하세요.
접수 담당자가 방문자 신분증을 확인하지 않아 누구나 전용 창구에 들어갈 수 있는 상황입니다. Origin은 그 신분증과 같습니다. 저장된 API 키를 쓸 수 있는 연결을 열기 전에 프록시가 이를 확인해야 합니다.
핵심 개념 및 용어
WebSocket- HTTP Upgrade 요청으로 시작해 양방향으로 메시지를 주고받는 지속 연결입니다.
Origin 헤더- 요청을 시작한 페이지를 식별하기 위해 브라우저가 보내는 스킴, 호스트, 포트입니다. WebSocket 핸드셰이크에서는 서버가 자체 신뢰 정책을 적용해야 하며 CORS 헤더가 이 검사를 대신하지 않습니다.
CSWSH- Cross-Site WebSocket Hijacking: 신뢰할 수 없는 사이트가 접근 가능한 서비스에 WebSocket을 열고, 서비스가 연결 시작 Origin을 검증하지 않는 문제입니다.
암묵적 자격 증명- 환경 변수의 API 키처럼 프로세스가 이미 사용할 수 있어 클라이언트 자체 자격 증명이 없는 요청에도 이용될 수 있는 비밀입니다.
CWE-1385- WebSocket Origin 검증 누락: 엔드포인트가 통신 출처가 허용되는지 확인하지 않습니다.
프레임워크 매핑
- 이 가이드에서 설명하는 약점의 CWE 참조: CWE-1385
- OWASP GenAI 2025 — LLM06:2025 과도한 에이전시
근본 원인 분석 (Root Cause)
Headroom의 /v1/responses WebSocket 핸들러는 핸드셰이크의 Origin을 확인하기 전에 브라우저 연결을 수락했습니다. 이후 프록시는 자체 환경의 OPENAI_API_KEY를 업스트림 요청에 추가할 수 있었습니다. 브라우저의 동일 출처 정책만으로 JavaScript가 다른 접근 가능한 호스트에 WebSocket 연결을 시작하는 것을 막을 수는 없습니다. 브라우저는 일반적으로 Origin을 보내므로 서버가 정책에 맞지 않는 Origin을 거부해야 합니다. 따라서 피해자 브라우저가 취약한 Headroom 프록시에 접근할 수 있고, 프록시에 유용한 자격 증명이나 통합이 있어야 악용할 수 있습니다. 권고에 따르면 수락된 연결은 임의의 LLM 요청을 보낼 수 있습니다. 호스트 명령 실행은 요청이 명령 실행 기능을 가진 구성 도구를 사용할 수 있을 때만 가능하며, 헤더 검사 누락만으로 자동 발생하지 않습니다. CNA는 0.35.0 이전 버전을 영향 범위로 제시하지만 GitHub 권고 패키지 메타데이터에는 0.23.0이 표시됩니다. 0.35.0 이상으로 업데이트하고 설치된 버전을 확인하세요.
단계별 공격 실행 흐름
프록시가 업스트림 권한을 보유
Headroom 프로세스는 OPENAI_API_KEY에 OpenAI 키를 보유할 수 있으며 브라우저 클라이언트가 키를 직접 보낼 필요는 없습니다.
브라우저가 신뢰할 수 없는 페이지를 엶
브라우저가 Headroom WebSocket 엔드포인트에 접근할 수 있으면 페이지 스크립트가 연결을 시도할 수 있습니다. 브라우저는 핸드셰이크에 페이지 Origin을 포함합니다.
취약한 핸들러가 Origin 정책 없이 연결을 수락
영향받는 핸들러는 신뢰할 수 없는 Origin을 먼저 거부하지 않고 websocket.accept()를 실행합니다. 동일 출처 정책은 서버 측 WebSocket 인가 검사가 아닙니다.
프록시가 인증된 요청을 전달
Headroom은 누락된 업스트림 Authorization 헤더를 환경 값으로 채울 수 있습니다. 공격자가 제어하는 페이지가 프록시 권한으로 요청을 발생시킬 수 있으며 영향은 도구, 자격 증명, 네트워크 접근성, 사용자 동작에 따라 달라집니다.
수정은 라우팅 전에 허용되지 않은 Origin을 거부
업스트림 패치는 세션을 수락하고 전달하기 전에 Origin을 검사합니다. 브라우저 Origin은 기본적으로 loopback으로 제한되고 명시적 Origin을 설정할 수 있으며 거부된 연결은 1008 코드로 닫힙니다.
소스 코드 비교: 취약한 구현 vs 보안 패치
권고는 handle_openai_responses_ws()에서 websocket.accept() 전에 검사가 누락된 점을 밝힙니다. 업스트림 commit c632023은 _is_allowed_websocket_origin()을 추가해 라우팅 전에 허용되지 않은 Origin을 거부합니다. Origin이 없는 네이티브 클라이언트는 계속 지원하고, 브라우저 Origin은 기본적으로 loopback만 허용하며 HEADROOM_WS_ORIGINS 또는 HEADROOM_CORS_ORIGINS로 명시 설정할 수 있습니다. 이 축약 코드는 바뀐 신뢰 경계를 보여주며 보조 함수를 모두 포함하지는 않습니다. 업스트림 회귀 테스트는 Origin 누락, loopback, 원격, 잘못된 형식, 명시 설정을 다룹니다.
소스 코드 발췌: headroom/proxy/handlers/openai.py
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
# 취약한 형태: origin을 검증하기 전에 브라우저 세션을 수락합니다.
await websocket.accept()
await self._relay_responses_session(websocket)
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
ws_headers = dict(websocket.headers)
if not _is_allowed_websocket_origin(ws_headers):
logger.warning(
"event=websocket_origin_not_allowed origin=%r",
_header_get(ws_headers, "origin"),
)
await websocket.close(code=1008, reason="origin not allowed")
return
# 정책에서 승인한 브라우저 세션만 업스트림 라우팅에 도달합니다.
await websocket.accept()
await self._relay_responses_session(websocket)
엔지니어링 및 시스템 보안 강화 체크리스트
- ✓Headroom을 0.35.0 이상으로 업데이트하고 의존성 파일뿐 아니라 설치 패키지와 배포 이미지 버전을 확인하세요.
- ✓
accept()또는 업스트림 전달 전에 WebSocketOrigin을 검사하고 명시적으로 허용되지 않은 non-loopback Origin을 거부하세요. - ✓Origin이 없는 네이티브 클라이언트는 유형을 별도로 신뢰할 때만 허용하세요. 브라우저 Origin이 없다는 것은 인가가 아닙니다.
- ✓자격 증명을 가진 프록시의 허용 Origin에
*를 사용하지 말고 신뢰할 Origin을 정확히 열거한 뒤 기본 포트 정규화를 테스트하세요. - ✓가능하면 로컬 개발 프록시를 loopback에 바인딩하고 다른 호스트가 접근할 수 있다면 인증 또는 네트워크 제어를 요구하세요.
- ✓가능한 경우 권한과 유효 기간이 제한된 업스트림 키를 사용하고 브라우저 경계를 넘어오는 요청에 shell 등 고영향 도구를 노출하지 마세요.
- ✓원격 Origin이 업스트림 라우팅 전에 정책 위반으로 종료되는 회귀 테스트를 추가하고 loopback, 설정 Origin, 승인된 네이티브 클라이언트도 확인하세요.
- ✓명령 실행은 구성된 도구 기능에 따라 달라집니다. Origin 결함 자체를 자동 호스트 RCE라고 표현하지 마세요.