LiteLLM 1.107.0 saldırısı, yaygın kullanılan bir LLM gateway’inin resmî yayın sürecinden 21 Ekim 2026’da kimlik bilgisi çalan bir sürümün çıkmasıdır. Zararlı paket PyPI’da 1 saat 44 dakika, Docker etiketlerinde 2 saat 34 dakika yayında kaldı. Paketi kuran makinede Python her başladığında zararlı kod çalıştı; uygulamanın LiteLLM’i içe aktarmasına bile gerek yoktu.
Olay yalnız LiteLLM kullananları ilgilendirmiyor. LLM gateway, bir kurumun model sağlayıcısı anahtarlarını tek bir süreçte toplar; bu yüzden saldırganlar için değerli bir hedeftir. Aşağıdaki önlemlerin çoğu kendi yazdığınız gateway için de, başka bir açık kaynak bağımlılık için de geçerlidir.
Yazı önce etkilenip etkilenmediğinizi nasıl anlayacağınızı anlatıyor. Ardından saldırının nasıl işlediğini deneyle gösteriyor ve hangi önlemin neyi durdurabileceğini tartışıyor. Olay bilgileri ağırlıklı olarak LiteLLM’in yayımladığı analizden ve bağımsız güvenlik ekiplerinin raporlarından alındı; 6 Kasım 2026 itibarıyla günceldir. Başlangıç denemesi Python 3.14.10 ile, paket araçlarıyla ilgili denemeler pip 26.3, uv 0.13.7 ve pip-audit 2.10.1 ile yapıldı. Zararlı sürüm indirilmedi; başlangıç mekanizmasını yalnızca ekrana bir satır yazan kendi deneme paketimle sınadım.
Kısaca
- Etkilenme ölçütü: 21 Ekim’de 11:39–13:23 UTC arasında sürümü sabitlemeden
litellmkurduysanız ya da 11:46–14:20 UTC arasındav1.107.0,latest,main-latestveyamain-stableimajını çalıştırdıysanız makineyi ele geçirilmiş sayın. O aralıkta türetilmiş imajlar ve iç aynalar bu saatlerin dışında da risk taşır. - Python’u çalıştırmadan inceleyin: Denemede
pip showvepip uninstallbile başlangıç kodunu çalıştırdı. Dosyaları kabuk komutlarıyla arayın, ortamı sıfırdan kurun. - Kod incelemesi bunu yakalayamazdı: GitHub’daki
v1.107.0etiketi temizdi. Zararlı dosyalar yalnızca yayımlanan paket ve imajlardaydı. - En ucuz önlem üç parçadan oluşur: hash’li kilit dosyası, digest ile sabitlenmiş imaj ve yeni sürümler için bekleme süresi.
- Kurulumu engellemek her şeyi çözmez: Zararlı kod gateway ile aynı kimlikle çalışırsa gateway’in okuduğu her şeyi okur. Zararı yalnızca kendi ağından kullanılabilen bulut kimliği, çıkış trafiğini kısıtlayan izin listesi ve harcama alarmı sınırlar.
LiteLLM 1.107.0 saldırısında ne oldu?
Saldırı paket deposunda değil, aylar önce sızan bir anahtarla başladı. LiteLLM’in 23 Ekim’de yayımladığı analize göre zincir şöyle ilerledi:
- 27 Mayıs: Agent Platform yönetici anahtarını içeren bir yapılandırma dosyası herkese açık bir depoya girdi. Dosya kısa sürede silindi ama anahtar iptal edilmedi.
- 10 Ekim: Saldırgan bu anahtarla Agent Platform API’sini çağırdı. Yönetilen ajanların yapılandırmasını döndüren yanıt, bir organizasyon sahibinin GitHub CLI token’ını da içeriyordu.
- 10 Ekim, aynı gece: Bu token’la CI iş akışları çalıştırıldı, iki geliştiricinin daha token’ı alındı ve çalıştırma kayıtları silindi.
- 21 Ekim, 10:08: Yayın iş akışına, derlenecek deponun dışarıdan seçilmesine izin veren bir değişiklik gönderildi. Dal koruması ilk denemeyi reddetti; sahip token’ıyla koruma kapatıldı.
- 21 Ekim, 10:46–11:11: Yayın süreci,
BerriAI/litellmyerine adı yalnızca bir tireyle ayrılanBerri-AI/litellmfork’undan derleme yaptı. Yayın onayı ikinci bir geliştiricinin çalıntı token’ıyla verildi. Onay kuralı o hafta geçici olarak gevşetilmişti. - 21 Ekim, 11:39 ve 11:46: Yedi wheel PyPI’a, imajlar Docker Hub ve GHCR’a yüklendi. Paketleri resmî yayın süreci imzaladı.
- 21 Ekim, 12:06–14:20: Dış bir araştırmacı durumu GitHub’da duyurdu. PyPI sürümü 13:23’te karantinaya aldı, Docker etiketleri 14:20’de temizlendi.
İlk haberlerde token’ların bir dağıtımın loglarından alındığı yazıyordu. LiteLLM 23 Ekim analizinde bunu düzeltti: ilk token log’dan değil, Agent Platform API yanıtından geldi. Eski haberleri okurken bu farka dikkat edin.
Bu, LiteLLM’in 2026’daki ikinci tedarik zinciri olayıydı. İki olay birbirine benzese de saldırganlar farklı noktalardan girdi:
| Mart 2026 | Ekim 2026 | |
|---|---|---|
| Sürümler | 1.82.7 ve 1.82.8 | 1.107.0 |
| Yayın yolu | CI atlanarak PyPI’a doğrudan yükleme | Resmî süreç, fork’tan derleme |
| Yayında kalma | Yaklaşık 40 dakika | PyPI 1 sa 44 dk, Docker 2 sa 34 dk |
| Resmî Docker imajı | Etkilenmedi | Etkilendi |
| Zararlı kodun yeri | proxy_server.py, 1.82.8’de ayrıca .pth | cook.pth, cook.py, sitecustomize.py |
Mart’tan sonra LiteLLM yayın sürecini yeniledi. Ekim saldırısı ise bir CI bağımlılığını değil, bu süreci yönetme yetkilerini hedef aldı. Bu yüzden çıkarılacak ders de farklı: Yayın sürecini güvenli hâle getirmek gerekir ama yetmez; süreci kimlerin değiştirebileceğini de sınırlamak gerekir.
LiteLLM saldırısından etkilendiğinizi nasıl anlarsınız?
Ölçüt iki soruya dayanır: Sürümü sabitlemeden litellm kuran bir iş yukarıdaki saat aralığında çalıştı mı? Yukarıdaki etiketlerden biri o aralıkta çekildi mi? LiteLLM başka bir aracın bağımlılığı olarak da gelmiş olabilir, bu yüzden CI kayıtlarınıza ve kilit dosyalarınıza da bakın.
Bu kontrolü Python’u çalıştırmadan yapın. Bir sonraki bölümdeki denemede pip show komutu bile başlangıç kodunu çalıştırdı.
# Python başlatmadan dosya sisteminde ara
find / \( -name 'cook.pth' -o -name 'litellm-1.107.0.dist-info' \) -not -path '/proc/*' 2>/dev/null
# Çalışmanın ve arka plan sürecinin izleri
ls -la /tmp/.pc_once_* /tmp/remote_code_loader.lock 2>/dev/null
# Kilit dosyalarında aday ara; Pipfile.lock paket adı ve sürümü ayrı satırda tutar
grep -rlE '1\.107\.0' --null --include='*.txt' --include='*.lock' --include='*.toml' . | xargs -0 grep -l 'litellm'
# Yerel imajlarda zararlı digest'i ara; kapsayıcı başlatmaz
docker images --digests | grep 1bbcb1443bc962e4
Kilit araması aday dosyaları listeler; eşleşen dosyada 1.107.0 değerinin gerçekten litellm paketine ait olduğunu gözle doğrulayın.
Bu saat aralığının dışında da etkilenmiş olabilirsiniz. İlk durum, bu imajlardan türetilen imajlardır: FROM ...:main-stable ile o aralıkta derlenmiş bir imaj zararlı katmanı bugün de taşır ve digest’i farklıdır, bu yüzden yukarıdaki digest araması onu bulmaz. İkincisi kurum içi paket aynaları ve imaj önbellekleridir: PyPI ve Docker Hub sürümü kaldırdıktan sonra da 1.107.0’ı sunmaya devam edip etmediklerini kontrol edin. Bir imajın dosyalarını Python’u çalıştırmadan şöyle listeleyebilirsiniz:
cid=$(docker create imajiniz:etiket)
docker export "$cid" | tar -t | grep -E 'cook\.pth|litellm-1\.107\.0\.dist-info'
docker rm "$cid"
sitecustomize.py dosyasını tek başına kanıt saymayın. Örneğin Ubuntu 26.04’te /usr/lib/python3.14/sitecustomize.py meşru olarak bulunur; site-packages içinde beklenmeyen bir kopyayı arıyorsunuz.
Bir şey bulmak etkilendiğinizi gösterir. Bulamamak ise temiz olduğunuzu kanıtlamaz: CI makineleri ve geçici kapsayıcılar çoktan yok olmuş olabilir. Etkilendiyseniz şu sırayı izleyin:
- Python ve pip çalıştırmayın. Her başlatma zararlı kodu yeniden tetikler.
- Ortamı sıfırdan kurun. İmajı ve sanal ortamı yeniden oluşturmak, temizlemekten daha güvenlidir. Temizlemek zorundaysanız
site-packagesiçindencook.pth,cook.py,sitecustomize.pyve bunların__pycache__kopyalarını elle silin,/tmp/remote_code_loader.lockdosyasını tutan arka plan sürecini durdurun. - Temiz bir makineden sırları değiştirin. LiteLLM özellikle instance ya da task IAM rolünü, proxy veritabanındaki bütün sağlayıcı anahtarlarını, master ve sanal anahtarları,
DATABASE_URLparolasını ve süreçlerin ortam değişkenlerindeki sırları sayıyor. Zararlı kodun topladıkları arasında SSH ve GPG anahtarları, Git ve bulut kimlikleri, tarayıcı oturumları ve yapay zekâ aracı token’ları da var. - Kayıtları inceleyin. CloudTrail’de kurulum zamanı civarındaki
GetCallerIdentityçağrılarına ve rol kimliklerinin altyapınız dışından kullanımına bakın. - Sürümü bilinçli seçin. LiteLLM olay gününde
1.106.2sürümünü öneriyordu. Bugün için güncel bildirimi kontrol edin ve seçtiğiniz sürümü sabitleyin.
Sızıntıdan sonra izlenecek genel sıra frontend API anahtarı yazısındakiyle aynıdır: önce iptal, sonra temizlik.
Python paketi içe aktarılmadan nasıl kod çalıştırır?
Olayın en şaşırtıcı kısmı budur. Uygulama import litellm yazmasa da kod çalıştı. Bunu mümkün kılan, Python’un başlangıç sırasıdır:
- Python başlarken
sitemodülünü yükler. site,site-packagesiçindeki.pthdosyalarını okur. Bu dosyalar normalde içe aktarma yolu eklemek içindir.- Bir
.pthsatırıimportile başlıyorsa Python o satırı doğrudan çalıştırır. - Ardından varsa
sitecustomizemodülünü içe aktarır.
SafeDep’in analizine göre zararlı wheel üç dosya bırakıyordu: 12 baytlık cook.pth, kimlik bilgilerini çalan 22 KB’lık cook.py ve arka kapı yükleyen sitecustomize.py. cook.pth dosyasının içeriği tek satır: import cook. Bu satırı kendim yazıp SHA-256 aldığımda SafeDep’in yayımladığı 7f191632… değerinin aynısı çıktı.
Deney: hangi komut başlangıç kodunu çalıştırır?
Aynı düzeni zararsız bir pakette kurdum. Deneme paketi, demo_startup.pth ile yalnızca stderr’e bir satır yazan demo_startup.py dosyasını bırakıyor. Paketi kurup her komutta bu satırın görünüp görünmediğine baktım:
| Komut | Başlangıç kodu çalıştı mı? |
|---|---|
python -c pass | Evet |
python -E -c pass, -s, -I, -P | Evet |
python -S -c pass | Hayır |
pip --version, pip list, pip show | Evet |
pip uninstall -y demo-gateway | Evet, dosyaları silmeden önce |
Paket dizini silindikten sonra python -c pass | Evet |
| Aynı makinede başka bir sanal ortam | Hayır |
Çalışan süreçte demo_gateway modülü yüklü değildi, demo_startup yüklüydü. sitecustomize.py ile aynı deney -c pass ve -I altında çalıştı, -S altında çalışmadı. Python 3.10.16 ile yaptığım ilk deneme de ortak komutlarda aynı sonucu verdi.
Bu tablodan üç pratik sonuç çıkıyor:
-Ikorumaz. İzole mod ortam değişkenlerini, kullanıcısitedizinini ve çalışma dizinini dışarıda bırakır, amasite-packagesiçindeki.pthdosyalarını okumaya devam eder. Bu yüzden güvenli bir denetim için yeterli değildir.- Teşhis komutu da kodu çalıştırır.
pip show, zararlı kodu bir kez daha çalıştırır. Sürümü*.dist-infodizininin adından okuyun.python -S -m pipde çözüm değil: sanal ortamda pip’i bulamadı. - Paket dizinini silmek yetmez.
.pthve yanındaki dosyasite-packageskökünde kalır. Denemedepip uninstallbu dosyaları sildi, çünkü paketin dosya listesindeydiler; ama silmeden önce kodu bir kez daha çalıştırdı.
Python bu açığı kapatıyor, ama yavaş ilerliyor. PEP 829 .pth içindeki import satırlarını Python 3.15 ile kullanımdan kaldırıyor. Satırlar 3.17’ye kadar çalışmaya devam ediyor, 3.18’den itibaren yok sayılacak. sitecustomize.py bu değişikliğin kapsamında değil ve Ekim saldırısı iki yolu da kullandı.
Kurulumdan sonra başlangıç dosyalarını denetlemek
Paket kurulduktan sonra, ortamdaki Python ilk kez başlamadan önce bu dosyalara bakabilirsiniz. Meşru örnekler de var: denemede setuptools paketinin distutils-precedence.pth dosyası ve coverage paketinin a1_coverage.pth dosyası import satırı taşıyordu. Bu yüzden kara liste değil, hash’li izin listesi gerekir.
İlk denememde denetimi Python ile yazıp python -S ile çalıştırdım ve yanıldım. Python 3.10’da sanal ortam içinde -S kullanılınca site.getsitepackages() sanal ortamı değil temel kurulumu gösterdi ve zararlı dosya görünmedi. Python belgesine göre sys.prefix değeri ancak 3.14’ten itibaren -S seçeneğinden etkilenmiyor. Bu nedenle aşağıdaki betik Python’u hiç çalıştırmıyor, denetlenecek dizini parametre olarak alıyor:
#!/usr/bin/env bash
# Python başlatmadan, başlangıçta kod çalıştırabilen dosyaları izin listesiyle karşılaştırır.
# Kullanım: python-baslangic-denetimi.sh <site-packages> <izin-listesi>
set -euo pipefail
site_dir="$1"
allow_list="$2"
unknown=0
for file in "$site_dir"/*.pth "$site_dir"/sitecustomize* "$site_dir"/usercustomize*; do
[ -e "$file" ] || continue
case "$file" in
*.pth) grep -qE '^import[[:space:]]' "$file" || continue ;;
esac
name=$(basename "$file")
if [ -f "$file" ]; then
hash=$(sha256sum "$file" | cut -d' ' -f1)
else
hash="dizin"
fi
if grep -qxF "$hash $name" "$allow_list"; then
echo "izinli $name"
else
echo "BILINMEYEN $name $hash"
unknown=1
fi
done
exit "$unknown"
İzin listesi, gözden geçirilmiş temiz bir kurulumdan bir kez üretilip depoya eklenir. Bağımlılık güncellenince hash değişir ve betik durur; bu bir hata değil, değişikliği gözden geçirmeniz gerektiğinin işaretidir.
(cd .venv/lib/python3.12/site-packages && sha256sum distutils-precedence.pth) > startup-allowlist.txt
bash python-baslangic-denetimi.sh .venv/lib/python3.12/site-packages startup-allowlist.txt
Betiği sekiz senaryoda sınadım. Temiz ortam çıkış kodu 0 verdi. import cook satırlı cook.pth, yeni sitecustomize.py, kaynaksız sitecustomize.pyc, sitecustomize/ dizini ve izin listesindeki dosyanın değişmiş hâli çıkış kodu 1 verdi. Boş izin listesi meşru dosyaları da bilinmeyen saydı. Yalnız yol içeren .pth dosyası ise doğru biçimde görmezden gelindi.
Betiğin sınırı nettir: yalnız başlangıçta çalışan dosyalara bakar. Mart’taki 1.82.7 gibi zararlı kodu sıradan bir modülün içine koyan sürümü görmez.
Sürüm numarasına değil içeriğe güvenmek: hash ve digest
Kurulumun neye dayandığını aynı deneme paketiyle üç biçimde sınadım:
| Gereksinim | Kayıttaki durum | Sonuç |
|---|---|---|
demo-gateway>=1.0 | Yeni 1.1.0 ele geçirilmiş | 1.1.0 kuruldu, kod çalıştı |
demo-gateway==1.0.0 | Aynı sürümün içeriği değişmiş | Kuruldu, kod çalıştı |
--require-hashes | Aynı sürümün içeriği değişmiş | pip çıkış kodu 1, hiçbir şey kurulmadı |
İlk satır Ekim saldırısının ta kendisidir. LiteLLM’in etkilenme tanımı sürümü sabitlememiş kurulumları kapsıyor; ==1.106.2 yazan biri yeni sürümü almadı.
İkinci satır PyPI için gerçekçi değil, çünkü PyPI aynı dosya adının yeniden yüklenmesine izin vermiyor. Aynı risk Docker etiketlerinde vardır. Ekim’de latest ve main-stable etiketleri önce zararlı imaja yöneldi, sonra temiz imajlara çevrildi ya da kaldırıldı. Etiket değişebilir bir işaretçidir, digest ise içeriğin kendisine bağlıdır.
Python tarafında kilit dosyası şöyle üretilir. requirements.in yalnız doğrudan bağımlılıkları taşır:
uv pip compile --generate-hashes --exclude-newer '7 days' requirements.in -o requirements.txt
pip install --require-hashes --only-binary :all: -r requirements.txt
litellm>=1.100 tek satırından bu komut 3 Kasım’da 58 paket ve 1.901 hash üretti. --only-binary :all: kaynak dağıtımların kurulum sırasında kod çalıştırmasını da engeller.
İmajın digest değerini Docker çalıştırmadan imaj deposundan (registry) alabilirsiniz:
TOKEN=$(curl -s 'https://ghcr.io/token?scope=repository:berriai/litellm:pull' | jq -r .token)
curl -sI -H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/vnd.oci.image.index.v1+json' \
https://ghcr.io/v2/berriai/litellm/manifests/v1.106.2 | grep -i docker-content-digest
Ardından dağıtımda etiket yerine bu değeri kullanın:
services:
gateway:
image: ghcr.io/berriai/litellm@sha256:<digest>
Hash’li kilidin bir sınırı var. Kilidi saldırı sürerken yeniden üretirseniz zararlı sürümün hash’i de kilide girer. Kilit yalnız daha önce gözden geçirdiğiniz içeriği korur; yeni sürümün güvenli olduğunu söylemez. Bu boşluğu bekleme süresi kapatır.
Yeni sürümü hemen almamak: bekleme süresi
Zararlı sürüm PyPI’da iki saatten kısa kaldı. Dış bir araştırmacı yarım saat içinde durumu duyurmuştu. Bağımlılık çözücüsüne “son yedi günde yayımlanan sürümleri hesaba katma” demek, böyle kısa bir saldırı süresini tamamen dışarıda bırakır.
# pip: yalnız yedi günden eski yüklemeleri değerlendir
pip install --uploaded-prior-to P7D -r requirements.in
# uv: aynı sınır, göreli süre ya da sabit tarih alır
uv pip compile --exclude-newer '7 days' requirements.in -o requirements.txt
litellm>=1.100 için pip bu sınırla 3 Kasım’da 1.106.4 sürümünü seçti. Sınırı saldırıdan yedi gün önceye, 14 Ekim 11:39 UTC’ye çektiğimde 1.105.1, saldırıdan hemen önceye çektiğimde 1.106.2 seçti. Aynı ayarı bağımlılık güncelleme aracınızda da açın:
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 7
Dependabot belgesine göre cooldown yalnız sürüm güncellemelerine uygulanır, güvenlik güncellemelerine uygulanmaz; docker ekosisteminde de default-days desteklenir. Renovate’te karşılığı minimumReleaseAge seçeneğidir. uv ise süreyi paketin yüklenme zamanından hesaplar; bu yüzden paket deposunun bu bilgiyi vermesi gerekir. PyPI veriyor, kurum içi aynanızı ayrıca kontrol edin.
Bekleme süresi ve kilit, güvenlik yamalarını da geciktirir. 21 Ekim öncesine kilitlediğim 58 paketlik dosyada pip-audit, 3 Kasım’da filelock için 12 bilinen güvenlik kaydı buldu. Bu yüzden kilitle birlikte düzenli güvenlik taraması çalıştırın ve kritik yamalar için bir istisna yolu tanımlayın. uv’de exclude-newer-package tek bir paketi bu kuraldan ayırır.
pip-audit önleyici değil, geriye dönük bir araçtır. Denemede litellm==1.107.0 için PYSEC-2026-2 kaydıyla çıkış kodu 1 verdi. Ancak ilgili OSV kaydı son olarak 29 Ekim’de güncellenmiş; saldırı sürerken sizi durdurması beklenemez. Yararı, sonradan bakıp geçmişte neyi kurduğunuzu görmektir.
İmzayı doğrulamak neden yetmedi?
Yazılım tedarik zinciri güvenliğinde ilk akla gelen öneri “imzayı doğrula” olur. Bu olayda yetmemesinin nedeni, imzanın başka bir soruyu yanıtlamasıdır.
İmza “bu paketi hangi kimlik üretti?” sorusunu yanıtlar. Kaynak kanıtı (provenance) ise paketin hangi depodan ve hangi iş akışından çıktığını kaydeder. Mart saldırısında paketler CI atlanarak doğrudan yüklenmişti. Yayıncı kaynak kanıtı üretiyor ve siz de bunu zorunlu tutuyorsanız bu fark görünür.
Ekim’de durum farklıydı. Paketleri LiteLLM’in kendi yayın süreci ürettiği için imzalayan kimlik doğruydu; yanlış olan, sürecin derlediği kaynak koddu. Datadog’a göre paketler, fork’u kaynak olarak doğru kaydeden ve görünüşte meşru bir kaynak kanıtıyla yayımlandı. Yani kanıt yalan söylemedi, ama yalnız imzanın geçerliliğine bakan biri bu ayrıntıyı görmezdi. Kayıtlı kaynağı beklenen depoyla tam eşleştirmek gerekir. Rapor bu kanıtın hangi pakete ve hangi biçime ait olduğunu ayrıntılı olarak vermiyor. LiteLLM’e göre fork’taki commit GitHub’da BerriAI/litellm adresi altında da görünüyordu, bu yüzden commit bağlantısına bakmak da yetmez.
İmza anahtarının kendisi de değişti. LiteLLM, saldırganın topladığı token’larla eski cosign anahtarının özel yarısının da açığa çıktığını düşünüyor ve 23 Ekim’de anahtarı yeniledi. Yeni anahtarla imzalanan ilk sürümler 24 Ekim’de çıkan v1.103.8, v1.104.6, v1.105.4 ve v1.106.3; sonraki bütün sürümler de bu anahtarı kullanıyor. İmajı cosign ile doğruluyorsanız elinizdeki cosign.pub dosyasının SHA-256 parmak izini olay sayfasındaki değerle karşılaştırın. Anahtar sızmış olabileceği için eski anahtarla atılmış bir imza tek başına güven kanıtı sayılmamalı.
Kaynak depo kontrolünü pypi-attestations ile denedim:
pypi-attestations verify pypi --repository https://github.com/pypa/sampleproject \
pypi:sampleproject-4.0.0.tar.gz
# OK: sampleproject-4.0.0.tar.gz
pypi-attestations verify pypi --repository https://github.com/py-pa/sampleproject \
pypi:sampleproject-4.0.0.tar.gz
# Verification failed: provenance was signed by repository "pypa/sampleproject", expected "py-pa/sampleproject"
Kontrol benzer depo adını yakalıyor. Ama elde doğrulanacak bir kanıt olması gerekir: litellm için 1.82.6, 1.105.7, 1.106.2 ve 1.106.6 sürümlerinde denediğim wheel dosyalarında PyPI “No provenance available” yanıtı verdi. Bu yüzden bu yolun Ekim’de PyPI kurulumunu durduracağını söyleyemem. Kullandığınız projenin hangi paketler için hangi kanıtı yayımladığını önceden kontrol edin.
Ekim saldırısını daha doğrudan yakalayan kontrol, yayımlanan paketi etiketlenmiş kaynak kodla karşılaştırmaktır. SafeDep ve Aikido’ya göre derleme yapılandırmasına data = "cook" satırı eklenmişti ve cook/ dizini GitHub’daki kaynakta yoktu. LiteLLM de GitHub dışında çalışan, yayımlanan paket ve imajları etiket içeriğiyle karşılaştırıp fark görünce sürümü geri çeken bir doğrulayıcıyı 31 Aralık’a kadar kuracağını açıkladı. İç paket aynası işleten kurumlar aynı karşılaştırmayı kendi tarafında yapabilir.
Kendi paketinizi ya da imajınızı yayımlıyorsanız Datadog’un önerisi açık: Yayın iş akışının yalnızca onaylı depolardan derleme yapmasını sağlayın ve geliştiricilerin CLI token’larını yüksek değerli sır olarak koruyun. Ekim’de saldırganın ilk işi, iş akışındaki bu depo kısıtını kaldırmak oldu.
Gateway’de hasarı nasıl sınırlarsınız?
Hiçbir önlem kusursuz değildir. Zararlı kod gateway ile aynı kullanıcıyla çalışıyorsa gateway’in okuduğu her şeyi okur. Veritabanındaki anahtarlar şifreli olsa bile gateway onları çözebildiği için zararlı kod da aynı yolu kullanabilir. SafeDep’e göre zararlı kod LiteLLM’in kimlik bilgisi tablolarını, diğer süreçlerin ortam değişkenlerini ve bulut meta veri servisini hedefliyordu.
Bu durumda amaç, çalınan bilginin değerini düşürmek ve dışarı çıkış yolunu daraltmaktır:
| Önlem | Ne yapar? | Ekim’deki karşılığı |
|---|---|---|
| Çıkış trafiği izin listesi | Gateway yalnızca model sağlayıcılarına ve veritabanına bağlanabilir | Veri www.litellm[.]dev adresine gidiyordu. Alan adı meşru göründüğü için engelleme listesi işe yaramazdı |
| Kendi ağına bağlı bulut kimliği | Statik anahtar yerine görev rolü kullanılır, rol kimliği yalnız kendi ağından kabul edilir | Çalınan AWS rol kimlikleri zaten geçiciydi ve yaklaşık 4,5 saat sonra denendi. Kısa ömürlü olmak tek başına yetmez; dışarıdan kullanımı ağ koşulu engeller |
| Meta veri servisini kapatmak | HttpTokens=required ve hop limit 1 ile köprü ağındaki kapsayıcı IMDS yanıtı alamaz | Zararlı kod 169.254.169.254 adresinden kimlik çekiyordu |
| Ortam ve kiracı başına anahtar | Her ortamın kendi sağlayıcı anahtarı ve harcama tavanı olur | Tek bir anahtar sızınca bütün canlı ortam etkilenmez |
| Geliştirici makinesini ayırmak | Gateway geliştirme sırasında da kapsayıcı içinde çalışır | Zararlı kod SSH anahtarlarını, tarayıcı oturumlarını ve yapay zekâ aracı token’larını topluyordu |
| Yönetim arayüzünü kapatmak | Yönetim API’si kimlik doğrulayan bir proxy arkasında durur | Zincir, yönetici anahtarının dışarıdan kullanılabilmesiyle başladı |
AWS’de EC2 rol kimlikleri için aws:Ec2InstanceSourceVpc koşulu bu işe yarar: çağrı, kimliğin teslim edildiği VPC’den gelmiyorsa reddedilir. Örnek politikalar IAM belgesinde; koşulun karşılaştırdığı aws:SourceVpc değeri yalnızca istek bir VPC uç noktasından geçtiğinde oluşur.
Hop limit ayarında AWS’nin notunu unutmayın: Kapsayıcıdaki iş IMDS’e gerçekten ihtiyaç duyuyorsa hop limit 1 çağrının yanıtsız kalmasına neden olabilir. Gateway kendi görev rolünü kullanıyorsa bu istenen davranıştır.
Yönetim API’si yanıtında sır döndürmemek
Zincirin dönüm noktası tek bir GET isteğiydi: yönetilen ajanları listeleyen uç nokta, yapılandırmayla birlikte bir GitHub token’ı da döndürdü. Kendi gateway’inizde yanıtı nesnenin tamamını kopyalayarak değil, yalnızca izin verilen alanlardan oluşturun:
// admin-public.mjs: yanıta yalnız açıkça izin verilen alanlar girer
const PUBLIC_AGENT_FIELDS = ["id", "name", "model", "status"];
export function toPublicAgent(agent) {
return Object.fromEntries(
PUBLIC_AGENT_FIELDS.filter(field => field in agent).map(field => [
field,
agent[field],
])
);
}
Bu kuralı bir kanarya testiyle koruyun. Test ortamının yapılandırmasına rastgele bir değer koyun ve hiçbir yanıtta görünmediğini doğrulayın:
// admin-public.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { toPublicAgent } from "./admin-public.mjs";
test("ajan listesi yanıtı yapılandırılmış sırları taşımaz", () => {
const canary = `kanarya-${randomUUID()}`;
const agent = {
id: "agent-1",
name: "pr-acici",
model: "ornek-model",
status: "active",
config: { githubToken: canary, env: { PROVIDER_KEY: canary } },
};
const body = JSON.stringify({ agents: [agent].map(toPublicAgent) });
assert.equal(body.includes(canary), false);
});
toPublicAgent yerine nesneyi olduğu gibi döndürdüğümde test başarısız oldu, izin listesiyle geçti. Aynı testi gerçek uç noktalara karşı da çalıştırın: her yönetim GET isteğinin gövdesinde kanaryayı arayın.
Bir not daha: 27 Mayıs’taki dosya kısa sürede silinmişti ama anahtar geçerli kaldı. Dosyayı silmek sızıntıyı kapatmaz, anahtarı iptal etmek kapatır. LiteLLM de artık sızan anahtar için, iptal doğrulanana kadar kapanmayan otomatik bir kayıt açacağını duyurdu.
Çalınan anahtarı nasıl fark edersiniz?
LLMjacking, çalınan kimlikle başkasının hesabından model çağırmaktır. Fatura kurbana, kullanım saldırgana kalır. Help Net Security’nin aktardığına göre Wiz, sızıntıdan yaklaşık dört buçuk saat sonra saldırganın CI ve Kubernetes düğümlerinden alınan AWS kimliklerini doğruladığını ve Amazon Bedrock modellerine erişimi yokladığını gördü.
Bu yüzden ilk alarm kuralları fatura değil, keşif hareketleri üzerine kurulmalıdır:
- Kimlik kontrolü: Beklenmeyen
sts:GetCallerIdentityçağrıları. - Model keşfi:
bedrock:ListFoundationModelsveGetFoundationModelAvailability. Gateway’in normal akışı bunları sık çağırmaz. - Model erişimi açma:
PutUseCaseForModelAccessvePutFoundationModelEntitlement. Daha önceki LLMjacking araştırmalarında görülen hareketlerdir. - Beklenmeyen kaynak: Rol kimliğinin altyapınız dışından kullanılması ya da uzun ömürlü anahtarla model çağrısı. Elastic’in tespit kuralları bu örüntüler için iyi bir başlangıç noktasıdır.
- Mutabakat farkı: Gateway kendi kullanımını ölçüyorsa sağlayıcı raporuyla karşılaştırın. Gateway yazısındaki mutabakat burada bir güvenlik göstergesine dönüşür: sağlayıcı, gateway’in saydığından belirgin biçimde fazlasını raporluyorsa biri anahtarı gateway dışında kullanıyor olabilir.
Sağlayıcı panelindeki harcama tavanı ve alarm, en basit son savunma hattıdır. Tavan saldırıyı durdurmaz ama faturanın büyüklüğünü sınırlar.
Kontrolleri CI’da birleştirmek
Aşağıdaki iş akışı bağımlılık değişikliklerinde ve her gün çalışır. Sıra önemlidir: paketler hash ile kurulur, .venv içindeki Python çalıştırılmadan önce başlangıç dosyaları denetlenir, güvenlik taraması ayrı bir araç ortamında yapılır. requirements-audit.txt yalnız pip-audit aracını hash’li olarak sabitler.
name: gateway-dependencies
on:
pull_request:
paths:
- requirements.txt
- startup-allowlist.txt
schedule:
- cron: "0 6 * * *"
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: "3.12"
- name: Kilit dosyasıyla kur
run: |
python -m venv .venv
.venv/bin/pip install --require-hashes --only-binary :all: -r requirements.txt
- name: Başlangıç dosyalarını denetle
run: bash scripts/python-baslangic-denetimi.sh .venv/lib/python3.12/site-packages startup-allowlist.txt
- name: Bilinen kayıtları tara
run: |
python -m venv .audit
.audit/bin/pip install --require-hashes -r requirements-audit.txt
.audit/bin/pip-audit --disable-pip --require-hashes --vulnerability-service osv -r requirements.txt
Bu iş akışının canlı ortam anahtarlarına erişimi olmamalıdır. Kurulumu yapan makine, başlangıç dosyası denetimi bitene kadar güvenilmez sayılmalı; ona sır vermeyin ve kalıcı önbellek paylaşmayın. Docker imajı üretiyorsanız aynı betiği pip install satırının hemen ardından, /usr/local/lib/python3.12/site-packages yoluyla çalıştırın.
Canlıya almadan önce kontrol listesi
- Gateway ve bağımlılıkları hash’li kilit dosyasıyla kuruluyor.
- Docker imajı etiketle değil digest ile çekiliyor.
- Yeni sürümler için en az birkaç günlük bekleme süresi var, kritik yama için istisna yolu tanımlı.
- Kurulum sonrası
.pthvesitecustomize.pydosyaları izin listesiyle denetleniyor. -
pip-auditya da eşdeğeri her gün çalışıyor. - Gateway’in çıkış trafiği izin listesiyle sınırlı.
- Bulut erişimi geçici ve kendi ağına bağlı kimlikle yapılıyor, gereksiz IMDS erişimi kapalı.
- Sağlayıcı anahtarları ortam ve kiracı başına ayrı, harcama tavanı tanımlı.
- Yönetim API’si internete kapalı, yanıtlarda sır olmadığını kanarya testi doğruluyor.
-
GetCallerIdentity, model keşfi ve mutabakat farkı için alarm var; anahtar değiştirme adımları yazılı ve denenmiş.
Sonuç
LiteLLM 1.107.0 saldırısı, kaynak kodu temiz, imzası geçerli ve resmî kanaldan gelen bir paketin de zararlı olabileceğini gösterdi. Tek bir önlem bu zinciri kırmaya yetmiyor. Kurulumu içeriğe bağlayan kilit ve digest, yeni sürümü hemen almamayı sağlayan bekleme süresi ve başlangıç dosyası denetimi saldırıyı önleyen ilk katmanı oluşturuyor. Kendi ağına bağlı kimlik, çıkış izin listesi ve erken alarm ise bu katman aşıldığında zararı sınırlıyor.
LiteLLM’in olay sonrası yayımladığı ayrıntılı analiz bu yazının da temelini oluşturuyor. Aynı riski taşıyan her gateway ekibi için asıl soru şudur: Bizim sistemimizde bu zincirin ilk halkası, iptal edilmemiş bir anahtar, bugün nerede duruyor?
Birincil kaynaklar
- LiteLLM: Contained, Oct 21, 2026 Release v1.107.0 Compromised
- LiteLLM: The Attack Chain Behind the v1.107.0 Release
- LiteLLM: What We Are Changing After October 21
- LiteLLM: Security Update, Suspected Supply Chain Incident (Mart 2026)
- SafeDep: Compromised LiteLLM 1.107.0 on PyPI
- Datadog Security Labs: LiteLLM Compromised on PyPI and Docker
- Aikido: LiteLLM Compromised, Infostealer and RAT
- Help Net Security: LiteLLM Hit by Second Supply Chain Attack This Year
- OSV: MAL-2026-2144
- PEP 829: Structured Startup Configuration Files
- Python: site, Site-specific Configuration Hook
- uv: Dependency Cooldowns
- GitHub: Dependabot Options Reference
- Renovate: minimumReleaseAge
- pypi-attestations
- AWS: Instance Metadata Access Considerations
- AWS IAM: aws:Ec2InstanceSourceVpc
- Elastic: AWS Bedrock Model Invocation Impossible Travel
- CSO Online: LLMjacking