2026년 10월 21일, LiteLLM 1.107.0이 PyPI와 공식 Docker 이미지로 배포되었습니다. GitHub의 v1.107.0 태그에는 악성 파일이 없었습니다. 그런데 같은 버전의 wheel에는 Python이 시작될 때마다 실행되는 credential stealer와 원격 코드 로더가 들어 있었습니다.
이 사건의 핵심은 “오픈소스 프로젝트를 믿을 수 없다”가 아닙니다. 소스 코드와 배포 artifact를 같은 것으로 취급하면 안 된다는 점입니다. 리뷰한 태그와 실제로 설치되는 wheel이 다를 수 있고, 서명과 provenance가 있어도 빌드가 바라본 저장소가 공격자 것일 수 있습니다.
2시간 동안 배포된 것
LiteLLM의 공식 사고 보고서는 영향을 받은 artifact와 시간대를 비교적 구체적으로 공개했습니다.
| Artifact | 영향을 받은 범위 | 노출 시간(UTC) |
|---|---|---|
PyPI litellm==1.107.0 |
7개 wheel 전체 | 11:39:51–13:23 |
| Docker Hub | v1.107.0, latest, main-latest, main-stable |
11:46:56–14:20 |
| GHCR | litellm, litellm-database, litellm-non_root의 같은 태그 |
11:46:56–13:51 |
Docker Hub의 문제 이미지 인덱스는 sha256:1bbcb1443bc962e45f5aad53fe3d2c30600e0379715fb6c664cdd3ac126c339e입니다. 공식 보고서는 이 digest를 실행한 컨테이너를 침해된 것으로 간주하라고 합니다.
중요한 예외도 있습니다. v1.107.0의 GitHub 소스 태그와 sdist는 payload를 포함하지 않았습니다. sdist의 pyproject.toml에는 data = "cook" 한 줄이 추가됐지만, cook/ 디렉터리 자체는 sdist에 들어가지 않았습니다. 그래서 소스 tarball만 검사하면 “깨끗하다”는 결론이 나옵니다. 실제로 실행되는 것은 wheel입니다.
왜 wheel과 소스가 갈라졌나
LiteLLM은 Rust 브리지를 포함한 platform wheel을 maturin으로 빌드합니다. maturin의 data 옵션은 로컬 디렉터리를 wheel의 .data/ 트리에 복사합니다. 공격자는 이 설정을 이용해 세 파일을 site-packages 최상위로 떨어뜨렸습니다.
| 파일 | 역할 |
|---|---|
cook.pth |
12바이트짜리 import cook. Python의 site 초기화 때 실행됩니다. |
cook.py |
자격증명을 수집해 암호화하고 외부로 보내는 stealer |
sitecustomize.py |
백그라운드 로더를 띄우고 주기적으로 원격 코드를 가져오는 파일 |
.pth와 sitecustomize.py는 Python 인터프리터가 시작될 때 자동으로 처리됩니다. import litellm을 하지 않아도 실행됩니다. pip, pytest, IDE의 language server, Jupyter kernel도 같은 환경이라면 영향을 받습니다. “우리 코드는 LiteLLM을 직접 import하지 않는다”는 말이 방어가 되지 않는 이유입니다.
독립 분석기관들은 payload의 세부 동작을 더 구체적으로 기록했습니다. SafeDep은 cook.py가 SSH 키, 클라우드 자격증명, CI/CD secret, 브라우저 비밀번호, AI 코딩 도구 token, LiteLLM proxy DB의 provider key를 수집한다고 분석했습니다. StepSecurity는 수집한 데이터를 AES-256-CBC로 암호화하고 RSA-4096으로 키를 감싼 뒤, 실제 LiteLLM 도메인과 비슷한 www.litellm[.]dev/healthcheck로 평문 HTTP 전송했다고 설명합니다.
로더는 GitHub의 dead-drop 파일에서 서버 주소를 읽고, 매시간 원격 코드를 받아 실행하도록 만들어졌습니다. 다만 분석 시점에 dead-drop은 비어 있었고, 해당 GitHub 계정은 삭제됐습니다. 로더가 실제 2차 payload를 받아 실행했다는 증거는 확인되지 않았습니다. 그렇다고 안전한 것은 아닙니다. 감염된 호스트는 계속 같은 주소를 폴링하므로, 나중에 같은 계정·저장소가 다시 만들어지면 공격 경로가 되살아날 수 있습니다.
Windows에서는 fork가 없어 로더가 조용히 실패합니다. 그러나 stealer는 동작합니다. 플랫폼별로 “로더가 실패했으니 괜찮다”고 판단하면 안 됩니다.
릴리스 파이프라인은 어떻게 뚫렸나
LiteLLM의 설명에 따르면 공격자는 먼저 인터넷에 노출된 내부 LiteLLM 배포에 접근했습니다. 그 로그에서 세 명의 개발자 GitHub CLI OAuth token을 얻었고, 그중 하나는 GitHub organization owner 권한을 가지고 있었습니다. 공격자는 branch protection을 해제하고 release workflow를 공격자 저장소인 Berri-AI/litellm을 바라보도록 바꿨습니다. 그다음 공식 파이프라인을 실행해 악성 wheel과 이미지를 빌드·서명·배포했습니다.
여기서 봐야 할 것은 “서명이 유효했다”는 사실입니다. 서명과 SLSA provenance는 artifact가 어떤 파이프라인에서 만들어졌는지를 보여줍니다. 그 파이프라인이 신뢰할 수 있는 저장소를 바라봤는지까지 자동으로 보장하지는 않습니다. StepSecurity는 v1.107.0 이미지의 provenance가 Berri-AI/litellm(하이픈이 들어간 유사 저장소)을 가리켰다고 분석했습니다. 정상 저장소는 BerriAI/litellm입니다.
공식 보고서는 token이 노출된 경로에 대해 “Claude Code가 GitHub CLI OAuth token을 응답 본문에 포함하는 상황이 있었고, 응답 본문을 로깅하면서 LiteLLM 로그에 남았다”고 주장합니다. 이 부분은 LiteLLM의 조사 결과이지, Anthropic이나 독립 기관이 확인한 사실로 제시된 것은 아닙니다. 사고의 핵심인 “조직 owner token으로 workflow 정의를 바꿨다”는 부분과 분리해서 읽어야 합니다. 원인 귀속은 아직 조사 중이고, 공격자가 처음 내부 배포에 어떻게 들어왔는지도 확정되지 않았습니다.
지금 확인할 것: Python을 실행하기 전에
공식 보고서는 확인 방법으로 pip show litellm을 제시합니다. 그런데 같은 문서가 “pip를 포함한 모든 Python 시작이 cook.pth와 sitecustomize.py를 다시 실행한다”고 경고합니다. 침해가 의심되는 환경에서는 pip show부터 실행하는 것이 모순입니다. 먼저 호스트를 격리하고, 셸 도구로 파일과 lockfile을 확인하는 편이 안전합니다.
아래 명령은 이 글에서 실행하지 않은 점검 예시입니다. 실제 환경에서는 경로와 권한을 확인한 뒤 읽기 전용으로 사용하세요.
# 1) site-packages에 추가된 startup hook 찾기
find / -type f \( \
-path '*/site-packages/cook.pth' -o \
-path '*/site-packages/cook.py' -o \
-path '*/site-packages/sitecustomize.py' \
\) 2>/dev/null
# 2) lockfile·requirements·CI 설정에서 정확한 버전 찾기
grep -RIn --include='requirements*.txt' --include='poetry.lock' \
--include='uv.lock' --include='Pipfile.lock' --include='pyproject.toml' \
'litellm.*1\.107\.0' . 2>/dev/null
# 3) 실행 중인 컨테이너의 이미지 digest 확인
docker image inspect --format '{{.RepoTags}} {{.RepoDigests}}' litellm:latest 2>/dev/null
sitecustomize.py는 배포판 Python이 정상적으로 제공하는 파일일 수도 있습니다. 이름만 보고 삭제하지 말고, 내용과 생성 시점을 확인해야 합니다. 반대로 파일이 없다고 해서 깨끗한 것도 아닙니다. 컨테이너와 임시 디렉터리는 정리됐을 수 있습니다.
침해가 확인되면 순서가 중요합니다.
- 호스트나 컨테이너를 네트워크에서 격리합니다.
- Python을 실행하지 말고 셸에서
cook.pth,cook.py,sitecustomize.py와__pycache__의 대응 파일을 제거합니다. /tmp/remote_code_loader.lock을 잡고 있는 분리된 Python 프로세스를 찾아 종료합니다.litellm==1.106.2로 고정하거나, 가능하면 이미지와 가상환경을 처음부터 다시 빌드합니다.- 호스트가 접근할 수 있었던 모든 자격증명을 교체합니다.
pip uninstall litellm을 먼저 실행하면 안 됩니다. 그것도 Python을 시작해 startup hook을 다시 실행합니다. “패키지를 지웠으니 됐다”가 아니라 “Python이 시작될 때 실행되는 파일을 지웠다”가 먼저입니다.
교체해야 할 자격증명
LiteLLM proxy는 여러 provider key를 한곳에 모으는 구조입니다. 그래서 개발자 노트북 한 대가 침해돼도 영향 범위가 넓습니다.
| 범위 | 예시 |
|---|---|
| 클라우드 | AWS instance/task role, GCP service account, Azure credential |
| LiteLLM | master key, virtual key, DATABASE_URL 비밀번호, provider key |
| 개발 도구 | GitHub CLI, SSH/GPG, package registry token |
| AI 도구 | Claude Code, Codex, Gemini CLI, Cursor 등의 인증 정보 |
| CI/CD | Vault token, GitLab Runner, Jenkins, Terraform/Packer credential |
공식 보고서는 AWS를 쓰는 경우 CloudTrail에서 설치 시점의 GetCallerIdentity 호출과 외부에서의 role 사용을 확인하라고 권고합니다. 이는 확정 탐지가 아니라 조사 단서입니다. 데이터가 실제로 사용됐는지까지는 로그만으로 알기 어렵습니다.
통제의 우선순위
이 사고는 “패키지 서명을 켜자”로 끝나지 않습니다. 공격자는 서명된 파이프라인을 이용했습니다. 방어선을 artifact, 빌드, 자격증명, 네트워크로 나눠야 합니다.
| 통제 | 막는 것 | 한계 |
|---|---|---|
| 버전과 hash 고정 | 새 릴리스가 자동으로 들어오는 일 | 이미 고정한 버전이 악성이면 소용없음 |
| digest로 이미지 고정 | latest 태그가 바뀌는 일 |
digest가 악성이면 여전히 위험 |
| provenance의 source 확인 | 유사 저장소에서 빌드된 artifact | 파이프라인 자체가 오염되면 우회 가능 |
| 짧은 수명의 배포 token | token 탈취 후 장기 악용 | workflow 승인 권한이 넓으면 여전히 위험 |
| release workflow 변경에 대한 별도 승인 | owner token 하나로 정의 변경 | 승인자도 같은 token을 쓰면 무력화 |
| egress allowlist | 알려진 exfil 도메인 | 미탐지 도메인과 DNS 우회 |
| 설치 cooldown | 공개 직후의 악성 버전 | 내부 미러가 즉시 동기화하면 우회 |
특히 “서명됐다”와 “신뢰할 수 있는 source에서 빌드됐다”를 분리해서 봐야 합니다. CI에서 provenance를 검사한다면 sourceRepository가 기대한 저장소인지, workflow가 보호된 branch에서 실행됐는지까지 확인해야 합니다. 이미지 서명 검증만으로는 이번 사고를 막지 못합니다.
이 사고가 말해주지 않는 것
확인된 사실과 추정을 구분해야 합니다. 공식 보고서와 독립 분석으로 확인된 것은 악성 artifact의 존재, 영향 시간, 파일 이름, exfil 경로, 배포 경로입니다. 반면 다음은 아직 확정되지 않았습니다.
- 공격자가 내부 LiteLLM 배포에 처음 접근한 정확한 취약점
- GitHub CLI token이 Claude Code 응답에 포함됐다는 LiteLLM의 설명이 일반적으로 재현되는지
- 탈취된 데이터의 실제 사용 여부와 규모
- 로더가 2차 코드를 실행한 사례가 있는지
- 조직별로 어떤 자격증명이 실제로 노출됐는지
이 불확실성 때문에 “영향 시간이 지났으니 괜찮다”는 판단은 위험합니다. 설치 이력이 있으면 노출을 가정하고 자격증명을 교체하는 쪽이 보수적입니다.
결론: 실행되는 artifact를 신뢰 경계로
LiteLLM 1.107.0 사고의 교훈은 “오픈소스를 쓰지 말자”가 아닙니다. 소스 태그, sdist, wheel, 컨테이너 이미지, 서명, provenance가 각각 다른 질문에 답한다는 사실입니다. 소스가 깨끗하다는 것은 wheel이 깨끗하다는 뜻이 아니고, 서명이 유효하다는 것은 빌드가 바라본 저장소가 올바르다는 뜻이 아닙니다.
당장 할 일은 세 가지입니다. 의존성 lockfile에서 litellm==1.107.0 설치 이력을 찾고, 실행 중인 컨테이너의 digest를 확인하고, 노출이 의심되는 환경의 자격증명을 교체하세요. 그다음 릴리스 파이프라인에서 “누가 workflow 정의를 바꿀 수 있는가”를 다시 설계해야 합니다.
함께 읽으면 좋은 글
- Agent가 패키지를 고르는 시대: GitLab Dependency Firewall 이해하기
- AI 에이전트 보안 입문: computer-use 시대에 권한 없이 실행하면 안 되는 이유
- CodeQL 2.27.2: 보안 분석이 잡는 것과 놓치는 것
참고 자료
- LiteLLM: Contained — Oct 21, 2026 LiteLLM release v1.107.0 Compromised
- GitHub: BerriAI/litellm issue #47826 — security incident
- SafeDep: Compromised LiteLLM 1.107.0 on PyPI installs a credential stealer and backdoor
- StepSecurity: litellm 1.107.0 compromised on PyPI
- Chainguard: LiteLLM attacked again — malicious 1.107.0 wheels on PyPI and Docker Hub