
CVE-2026-93355의 무해한 오프라인 재현 — LiteLLM 미검증 이메일 JWT 계정 탈취 (LiteLLM의 실제 코드 실행)
CVE-2026-93355를 재현하는 자체 포함형 일회용 실습 환경입니다: **LiteLLM**의 JWT 인증은 호출자의 신원을 이메일 일치로 대체하여 확인하며, email_verified 클레임을 절대 검사하지 않습니다. 공격자가 자신의 자체 신원 제공자 계정으로 피해자의 이메일 주소(미검증)를 담은 유효하게 서명된 토큰을 발급할 수 있다면, 피해자의 역할을 상속받아 proxy_admin까지 획득할 수 있으며, 이 일치 과정에서 피해자의 저장된 SSO 바인딩이 조용히 공격자의 subject로 재작성되어 탈취가 지속됩니다.
| 권고문 | VulnCheck · OX Security |
| CVE | CVE-2026-93355 |
| 프로젝트 | litellm (프록시 / JWT 인증) |
| 영향 범위 | 권고문 기준 ≤ 1.102.1 — 여전히 미패치: 취약한 코드는 2026-10-07 기준 1.104.0(최신 안정 버전)과 main에서 변경되지 않음 |
| CVSS | 8.1 HIGH (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N) |
| 약점 | CWE-1390 — 취약한 인증 |
| 근본 원인 | email_verified 검증 없이 이메일 기반 사용자 조회를 신뢰함 |
| 발견 | Nir Zadok & Moshe Siman Tov Bustan (OX Security) |
수정 상태 (2026-10-07): 아직 수정된 릴리스가 존재하지 않습니다. 권고문에
1.102.1만 나열된 것은 공개 시점의 최신 버전이었기 때문이며, LiteLLM은 이후1.103.x와1.104.0을 출시했지만 두 버전 모두 — 그리고main에서도 —_get_fuzzy_user_object에는 여전히email_verified검사가 없습니다. 실습 환경은 재현성을 위해1.102.1을 고정합니다;scripts/verify_against_real_source.py는 수정이 적용되는 날 이를 표시할 것입니다.
이것은 교육 및 방어 목적으로 공개적으로 공개된 취약점을 재현합니다. 전적으로 로컬에서 실행되며 실제 서비스에 접촉하지 않습니다:
[email protected] 관리자 행을 확인하고 출력할 뿐입니다. 실제 계정도, 실제 키도 없으며, 어떤 것도 사용자의 머신을 벗어나지 않습니다.LiteLLM의 프록시는 구성된 신원 제공자의 JWT로 인증할 수 있게 합니다. 해당 토큰을 LiteLLM 사용자에 매핑하기 위해 get_user_object()를 호출하는데, 이 함수는 토큰의 subject(sso_user_id)를 시도하고, 실패하면 _get_fuzzy_user_object()로 대체합니다 — 이는 email 클레임에 대한 대소문자 구분 없는 일치입니다. LiteLLM 테이블에서 해당 이메일을 소유한 사람이 곧 당신이 됩니다. 토큰의 email_verified 클레임은 절대 읽히지 않습니다 — 클레임 추출 시(JWTHandler.get_user_email)에도, 해석 시에도 마찬가지입니다. 따라서 미검증 이메일에 대해 토큰을 발급하는 모든 IdP(대부분은 설계상 그렇게 합니다 — 검증은 신뢰 당사자의 책임입니다)는 공격자가 자신의 프로필 이메일을 피해자의 것으로 설정하고 토큰을 제시하여 피해자의 역할을 상속받게 합니다. 더 나쁜 것은, 이메일 일치 시 LiteLLM이 백그라운드 작업을 실행하여 피해자의 sso_user_id를 공격자의 subject로 덮어쓴다는 점입니다 — 따라서 이후의 모든 로그인은 이메일 없이도 곧바로 피해자의 계정으로 해석되며, 이메일을 교체해도 도움이 되지 않습니다.

테스트 대상은 모두 LiteLLM 자체 코드이며, 고정된 litellm==1.102.1 패키지에서 가져옵니다. 실습 환경은 반드시 필요한 두 가지만 제공합니다: 가짜 인메모리 사용자 테이블과 모의 IdP 키페어입니다. 취약한 로직을 재구현하지 않습니다.
_get_fuzzy_user_object와 JWTHandler.get_user_email을 inspect.getsource()로 검사하고, 이메일 폴백이 존재하는지, sso_user_id 재바인딩이 존재하는지, 그리고 두 함수 모두 email_verified를 참조하지 않는지 검증합니다. → scripts/verify_against_real_source.py_get_fuzzy_user_object를 통과하며, 피해자의 proxy_admin 행으로 해석됩니다. → poc/01_account_takeover.pysso_user_id를 공격자의 subject로 재바인딩합니다. 이후 이메일이 전혀 없는 로그인은 곧바로 admin으로 해석되며, "중복 이메일 수정"으로는 이를 철회할 수 없습니다. → poc/02_persistent_rebind.pyemail_verified를 강제하면 검증된 SSO 로그인은 계속 작동하면서 공격을 차단합니다. → poc/03_patched_path.py./run.sh
run.sh는 일회용 .venv를 생성하고, litellm[proxy]==1.102.1을 설치한 뒤, ground-truth 검사를 실행하고, 이어서 세 가지 PoC를 모두 실행합니다. 첫 실행 시 LiteLLM을 다운로드하므로 몇 분 정도 걸립니다. 정리하려면 .venv를 삭제하세요. 기존 인터프리터를 사용하려면: PYTHON=python3.12 ./run.sh.
단일 PoC를 직접 실행하려면 (venv 내부에서):
.venv/bin/python poc/01_account_takeover.py
cve-2026-93355-lab/
├── mock_idp.py # ephemeral RS256 OIDC IdP (real signatures)
├── fake_litellm_db.py # in-memory stand-in for the Prisma user table
├── harness.py # configures & drives LiteLLM's real objects
├── poc/
│ ├── 01_account_takeover.py # unverified email -> victim's proxy_admin
│ ├── 02_persistent_rebind.py # sso_user_id overwrite -> persistent access
│ └── 03_patched_path.py # the email_verified guard that closes it
├── scripts/
│ └── verify_against_real_source.py # proves the PoCs run the real vulnerable code
├── assets/attack-flow.svg|.png # the diagram above
├── blog-cve-2026-93355.md # the guard0 write-up
└── run.sh
email_verified: true를 강제하고, 검증되지 않은 이메일은 거부하거나 무시하십시오.user_id_jwt_field)에 바인딩하고, 이메일 폴백이 이를 조용히 재바인딩하도록 두지 마십시오.proxy_admin 행은 상시적인 탈취 수단입니다.